diff --git a/Makefile b/Makefile index d3ddd15f359..7a5bfc7a7e9 100755 --- a/Makefile +++ b/Makefile @@ -152,6 +152,10 @@ push: sleep 1 $(MAKE) -C $(UPDATE_SERVER_DIR) push +.PHONY: push-folder +PUSH_HELPER := abr-testing/abr_testing/tools/make_push.py +push-folder: + $(OT_PYTHON) $(PUSH_HELPER) .PHONY: push-ot3 push-ot3: diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py index d43db612561..a61c16c7f46 100644 --- a/abr-testing/abr_testing/automation/jira_tool.py +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -107,6 +107,12 @@ def open_issue(self, issue_key: str) -> str: webbrowser.open(url) return url + def get_labels(self) -> List[str]: + """Get list of available labels.""" + url = f"{self.url}/rest/api/3/label" + response = requests.request("GET", url, headers=self.headers, auth=self.auth) + return response.json() + def create_ticket( self, summary: str, @@ -118,10 +124,12 @@ def create_ticket( priority: str, components: list, affects_versions: str, - robot: str, + labels: list, + parent_name: str, ) -> Tuple[str, str]: """Create ticket.""" # Check if software version is a field on JIRA, if not replaces with existing version + # TODO: automate parent linking data = { "fields": { "project": {"id": "10273", "key": project_key}, @@ -129,7 +137,8 @@ def create_ticket( "summary": summary, "reporter": {"id": reporter_id}, "assignee": {"id": assignee_id}, - "parent": {"key": robot}, + # "parent": {"key": parent_name}, + "labels": labels, "priority": {"name": priority}, "components": [{"name": component} for component in components], "description": { @@ -194,6 +203,7 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None def get_project_issues(self, project_key: str) -> Dict[str, Any]: """Retrieve all issues for the given project key.""" + # TODO: add field for ticket type. headers = {"Accept": "application/json"} query = {"jql": f"project={project_key}"} response = requests.request( @@ -203,7 +213,6 @@ def get_project_issues(self, project_key: str) -> Dict[str, Any]: params=query, auth=self.auth, ) - response.raise_for_status() return response.json() def get_project_versions(self, project_key: str) -> List[str]: 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 75b73b8f16b..46cc409e53d 100644 --- a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -286,6 +286,7 @@ def run( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() @@ -294,7 +295,7 @@ def run( ip_or_all = input("IP Address or ALL: ") calibration_data = [] if ip_or_all.upper() == "ALL": - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) for ip in ip_address_list: saved_file_path, calibration = read_robot_logs.get_calibration_offsets( ip, 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 6552534c4ae..8f82567a7d1 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -44,6 +44,7 @@ def create_data_dictionary( headers: List[str] = [] headers_lpc: List[str] = [] list_of_heights: List[List[Any]] = [[], [], [], [], [], [], [], []] + hellma_plate_orientation = False # default hellma plate is not rotated. for filename in os.listdir(storage_directory): file_path = os.path.join(storage_directory, filename) if file_path.endswith(".json"): @@ -67,6 +68,10 @@ def create_data_dictionary( if run_id in runs_to_save: print(f"started reading run {run_id}.") robot = file_results.get("robot_name") + parameters = file_results.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") software_version = file_results.get("API_Version", "") left_pipette = file_results.get("left", "") @@ -123,7 +128,7 @@ def create_data_dictionary( file_results, labware_name="opentrons_tough_pcr_auto_sealing_lid" ) plate_reader_dict = read_robot_logs.plate_reader_commands( - file_results, hellma_plate_standards + file_results, hellma_plate_standards, hellma_plate_orientation ) list_of_heights = read_robot_logs.liquid_height_commands( file_results, list_of_heights 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 f7a4237f52a..73cf12c6253 100644 --- a/abr-testing/abr_testing/data_collection/abr_robot_error.py +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -125,15 +125,20 @@ def compare_lpc_to_historical_data( & (df_lpc_data["Robot"] == robot) & (df_lpc_data["Module"] == labware_dict["Module"]) & (df_lpc_data["Adapter"] == labware_dict["Adapter"]) - & (df_lpc_data["Run Ending Error"] < 1) + & (df_lpc_data["Run Ending Error"]) + < 1 ] # Converts coordinates to floats and finds averages. - x_float = [float(value) for value in relevant_lpc["X"]] - y_float = [float(value) for value in relevant_lpc["Y"]] - z_float = [float(value) for value in relevant_lpc["Z"]] - current_x = round(labware_dict["X"], 2) - current_y = round(labware_dict["Y"], 2) - current_z = round(labware_dict["Z"], 2) + try: + x_float = [float(value) for value in relevant_lpc["X"]] + y_float = [float(value) for value in relevant_lpc["Y"]] + z_float = [float(value) for value in relevant_lpc["Z"]] + current_x = round(labware_dict["X"], 2) + current_y = round(labware_dict["Y"], 2) + current_z = round(labware_dict["Z"], 2) + except (ValueError): + x_float, y_float, z_float = [0.0], [0.0], [0.0] + current_x, current_y, current_z = 0.0, 0.0, 0.0 try: avg_x = round(mean(x_float), 2) avg_y = round(mean(y_float), 2) @@ -247,7 +252,7 @@ def get_error_runs_from_robot(ip: str) -> List[str]: f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} ) run_data = response.json() - run_list = run_data["data"] + run_list = run_data.get("data", []) for run in run_list: run_id = run["id"] num_of_errors = len(run["errors"]) @@ -258,7 +263,7 @@ def get_error_runs_from_robot(ip: str) -> List[str]: def get_robot_state( ip: str, reported_string: str -) -> Tuple[Any, Any, Any, List[str], str]: +) -> Tuple[Any, Any, Any, List[str], List[str], str]: """Get robot status in case of non run error.""" description = dict() # Get instruments attached to robot @@ -274,10 +279,11 @@ def get_robot_state( f"http://{ip}:31950/health", headers={"opentrons-version": "3"} ) health_data = response.json() - parent = health_data.get("name", "") + print(f"health data {health_data}") + robot = health_data.get("name", "") # Create summary name - description["robot_name"] = parent - summary = parent + "_" + reported_string + description["robot_name"] = robot + summary = robot + "_" + reported_string affects_version = health_data.get("api_version", "") description["affects_version"] = affects_version # Instruments Attached @@ -297,6 +303,12 @@ def get_robot_state( description[module["moduleType"]] = module components = ["Flex-RABR"] components = match_error_to_component("RABR", reported_string, components) + if "alpha" in affects_version: + components.append("flex internal releases") + labels = [robot] + if "8.2" in affects_version: + labels.append("8_2_0") + parent = affects_version + " Bugs" print(components) end_time = datetime.now() print(end_time) @@ -317,13 +329,14 @@ def get_robot_state( parent, affects_version, components, + labels, whole_description_str, ) def get_run_error_info_from_robot( ip: str, one_run: str, storage_directory: str -) -> Tuple[str, str, str, List[str], str, str]: +) -> Tuple[str, str, str, List[str], List[str], str, str]: """Get error information from robot to fill out ticket.""" description = dict() # get run information @@ -339,16 +352,19 @@ def get_run_error_info_from_robot( error_code = error_dict["Error_Code"] error_instrument = error_dict["Error_Instrument"] # JIRA Ticket Fields - + robot = results.get("robot_name", "") failure_level = "Level " + str(error_level) + " Failure" components = [failure_level, "Flex-RABR"] components = match_error_to_component("RABR", str(error_type), components) - print(components) affects_version = results["API_Version"] - parent = results.get("robot_name", "") - print(parent) - summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + if "alpha" in affects_version: + components.append("flex internal releases") + labels = [robot] + if "8.2" in affects_version: + labels.append("8_2_0") + parent = affects_version + " Bugs" + summary = robot + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type # Description of error description["protocol_name"] = results["protocol"]["metadata"].get( "protocolName", "" @@ -430,6 +446,7 @@ def get_run_error_info_from_robot( parent, affects_version, components, + labels, whole_description_str, saved_file_path, ) @@ -503,18 +520,20 @@ def get_run_error_info_from_robot( one_run = error_runs[-1] # Most recent run with error. ( summary, - robot, + parent, affects_version, components, + labels, whole_description_str, run_log_file_path, ) = get_run_error_info_from_robot(ip, one_run, storage_directory) else: ( summary, - robot, + parent, affects_version, components, + labels, whole_description_str, ) = get_robot_state(ip, run_or_other) # Get Calibration Data @@ -525,16 +544,8 @@ def get_run_error_info_from_robot( print(f"Making ticket for {summary}.") # TODO: make argument or see if I can get rid of with using board_id. project_key = "RABR" - print(robot) - try: - parent_key = project_key + "-" + robot.split("ABR")[1] - except IndexError: - parent_key = "" - - # Grab all previous issues - all_issues = ticket.issues_on_board(project_key) - # TODO: read board to see if ticket for run id already exists. + all_issues = ticket.issues_on_board(project_key) # CREATE TICKET issue_key, raw_issue_url = ticket.create_ticket( summary, @@ -546,7 +557,8 @@ def get_run_error_info_from_robot( "Medium", components, affects_version, - parent_key, + labels, + parent, ) # Link Tickets to_link = ticket.match_issues(all_issues, summary) 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 964a8a06e18..fe89f9f1543 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -104,10 +104,11 @@ def get_all_run_logs( ip_json_file = os.path.join(storage_directory, "IPs.json") try: ip_file = json.load(open(ip_json_file)) + robot_dict = ip_file.get("ip_address_list") except FileNotFoundError: print(f"Add .json file with robot IPs to: {storage_directory}.") sys.exit() - ip_address_list = ip_file["ip_address_list"] + ip_address_list = list(robot_dict.keys()) runs_from_storage = read_robot_logs.get_run_ids_from_google_drive(google_drive) for ip in ip_address_list: runs = get_run_ids_from_robot(ip) 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 40712118fe5..7bc83e0a54b 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -250,7 +250,9 @@ def liquid_height_commands( def plate_reader_commands( - file_results: Dict[str, Any], hellma_plate_standards: List[Dict[str, Any]] + file_results: Dict[str, Any], + hellma_plate_standards: List[Dict[str, Any]], + orientation: bool, ) -> Dict[str, object]: """Plate Reader Command Counts.""" commandData = file_results.get("commands", "") @@ -279,38 +281,46 @@ def plate_reader_commands( read = "yes" elif read == "yes" and commandType == "comment": result = command["params"].get("message", "") - formatted_result = result.split("result: ")[1] - result_dict = eval(formatted_result) - result_dict_keys = list(result_dict.keys()) - if len(result_dict_keys) > 1: - read_type = "multi" - else: - read_type = "single" - for wavelength in result_dict_keys: - one_wavelength_dict = result_dict.get(wavelength) - result_ndarray = plate_reader.convert_read_dictionary_to_array( - one_wavelength_dict - ) - for item in hellma_plate_standards: - wavelength_of_interest = item["wavelength"] - if str(wavelength) == str(wavelength_of_interest): - error_cells = plate_reader.check_byonoy_data_accuracy( - result_ndarray, item, False + if "result:" in result: + plate_name = result.split("result:")[0] + formatted_result = result.split("result: ")[1] + print(formatted_result) + result_dict = eval(formatted_result) + result_dict_keys = list(result_dict.keys()) + if len(result_dict_keys) > 1: + read_type = "multi" + else: + read_type = "single" + if "hellma_plate" in plate_name: + for wavelength in result_dict_keys: + one_wavelength_dict = result_dict.get(wavelength) + result_ndarray = plate_reader.convert_read_dictionary_to_array( + one_wavelength_dict ) - if len(error_cells[0]) > 0: - percent = (96 - len(error_cells)) / 96 * 100 - for cell in error_cells: - print( - "FAIL: Cell " + str(cell) + " out of accuracy spec." + for item in hellma_plate_standards: + wavelength_of_interest = item["wavelength"] + if str(wavelength) == str(wavelength_of_interest): + error_cells = plate_reader.check_byonoy_data_accuracy( + result_ndarray, item, orientation ) - else: - percent = 100 - print( - f"PASS: {wavelength_of_interest} meet accuracy specification" - ) - final_result[read_type, wavelength, read_num] = percent - read_num += 1 - read = "no" + if len(error_cells[0]) > 0: + percent = (96 - len(error_cells)) / 96 * 100 + for cell in error_cells: + print( + "FAIL: Cell " + + str(cell) + + " out of accuracy spec." + ) + else: + percent = 100 + print( + f"PASS: {wavelength_of_interest} meet accuracy spec." + ) + final_result[read_type, wavelength, read_num] = percent + read_num += 1 + else: + final_result = result_dict + read = "no" plate_dict = { "Plate Reader # of Reads": read_count, "Plate Reader Avg Read Time (sec)": avg_read_time, diff --git a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index f55c9ebb51f..76852f70b9c 100644 --- a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -46,7 +46,7 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: labware_defs = [] for root, directories, _ in os.walk(root_dir): for directory in directories: - if directory == "active_protocols": + if directory not in exclude: active_dir = os.path.join(root, directory) for file in os.listdir( active_dir @@ -100,7 +100,6 @@ def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: exclude = [ "__init__.py", "helpers.py", - "shared_vars_and_funcs.py", ] print("Simulating Protocols") file_dict, labware_defs = get_files() diff --git a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index 57695f03557..10c7ea12782 100644 --- a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -225,7 +225,11 @@ def parse_results_volume( else: print(f"Expected JSON object (dict) but got {type(json_data).__name__}.") commands = {} - + hellma_plate_orientation = False + parameters = json_data.get("runTimeParameters", "") + for parameter in parameters: + if parameter["displayName"] == "Hellma Plate Orientation": + hellma_plate_orientation = bool(parameter["value"]) start_time = datetime.fromisoformat(commands[0]["createdAt"]) end_time = datetime.fromisoformat(commands[len(commands) - 1]["completedAt"]) header = ["", "Protocol Name", "Date", "Time"] @@ -283,7 +287,7 @@ def parse_results_volume( temp_module_dict = read_robot_logs.temperature_module_commands(json_data) thermo_cycler_dict = read_robot_logs.thermocycler_commands(json_data) plate_reader_dict = read_robot_logs.plate_reader_commands( - json_data, hellma_plate_standards + json_data, hellma_plate_standards, hellma_plate_orientation ) instrument_dict = read_robot_logs.instrument_commands( json_data, labware_name=None @@ -499,12 +503,12 @@ def check_params(protocol_path: str) -> str: def get_extra_files(protocol_file_path: str) -> tuple[str, List[Path]]: """Get supporting files for protocol simulation if needed.""" params = check_params(protocol_file_path) - needs_files = input("Does your protocol utilize custom labware? (y/n): ") + needs_files = input("Does your protocol utilize custom labware? (Y/N): ") labware_files = [] - if needs_files == "y": + if needs_files == "Y": num_labware = input("How many custom labware?: ") for labware_num in range(int(num_labware)): - path = input("Enter custom labware definition: ") + path = input("Enter custom labware definition path: ") labware_files.append(Path(path)) return (params, labware_files) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py b/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py deleted file mode 100644 index 50fb82e94d5..00000000000 --- a/abr-testing/abr_testing/protocols/active_protocols/3_OT3 ABR Normalize with Tubes.py +++ /dev/null @@ -1,343 +0,0 @@ -"""FLEX Normalize with Tubes.""" -from opentrons.protocol_api import ProtocolContext, ParameterContext, Well -from abr_testing.protocols import helpers -from typing import List - -metadata = { - "protocolName": "Flex Normalize with Tubes", - "author": "Opentrons ", - "source": "Protocol Library", -} - -requirements = {"robotType": "Flex", "apiLevel": "2.21"} - -# SCRIPT SETTINGS -ABR_TEST = True -if ABR_TEST: - DRYRUN = True # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = ( - False # True = Used tips go in Trash, False = Used tips go back into rack - ) -else: - DRYRUN = False # True = skip incubation times, shorten mix, for testing purposes - TIP_TRASH = True - - -def add_parameters(parameters: ParameterContext) -> None: - """Parameters.""" - helpers.create_csv_parameter(parameters) - helpers.create_dot_bottom_parameter(parameters) - helpers.create_two_pipette_mount_parameters(parameters) - - -def run(ctx: ProtocolContext) -> None: - """Protocol.""" - mount_pos_50ul = ctx.params.pipette_mount_1 # type: ignore[attr-defined] - mount_pos_1000ul = ctx.params.pipette_mount_2 # type: ignore[attr-defined] - dot_bottom = ctx.params.dot_bottom # type: ignore[attr-defined] - parsed_csv = ctx.params.parameters_csv.parse_as_csv() # type: ignore[attr-defined] - if DRYRUN: - ctx.comment("THIS IS A DRY RUN") - else: - ctx.comment("THIS IS A REACTION RUN") - - # labware - tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "1") - tiprack_200_1 = ctx.load_labware("opentrons_flex_96_tiprack_200ul", "4") - reagent_tube = ctx.load_labware( - "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "5", "Reagent Tube" - ) - sample_plate = ctx.load_labware( - "armadillo_96_wellplate_200ul_pcr_full_skirt", "2", "Sample Plate" - ) - - # reagent - RSB = reagent_tube.wells()[0] - - # pipette - p1000 = ctx.load_instrument( - "flex_1channel_1000", mount_pos_1000ul, tip_racks=[tiprack_200_1] - ) - p50 = ctx.load_instrument( - "flex_1channel_50", mount_pos_50ul, tip_racks=[tiprack_50_1] - ) - - wells_with_liquids: List[Well] = [RSB] - helpers.load_wells_with_water(ctx, wells_with_liquids, [4000.0]) - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) - MaxTubeVol = 200 - RSBVol = 0.0 - - data = parsed_csv - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - if DilutionVol <= 1: - ctx.comment("Sample " + CurrentWell + ": Conc. Too Low, Will Skip") - elif DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Sample " - + CurrentWell - + ": Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - RSBVol += MaxTubeVol - InitialVol - else: - if DilutionVol <= 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p50, will add " - + str(round(DilutionVol, 1)) - ) - elif DilutionVol > 20: - ctx.comment( - "Sample " - + CurrentWell - + ": Using p1000, will add " - + str(round(DilutionVol, 1)) - ) - RSBVol += DilutionVol - current += 1 - - if RSBVol >= 14000: - ctx.pause("Caution, more than 15ml Required") - else: - ctx.comment("RSB Minimum: " + str(round(RSBVol / 1000, 1) + 1) + "ml") - - PiR2 = 176.71 - InitialRSBVol = RSBVol - RSBHeight = (InitialRSBVol / PiR2) + 17.5 - - ctx.pause("Proceed") - ctx.comment("==============================================") - ctx.comment("Normalizing Samples") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - - ctx.comment("Number " + str(data[current]) + ": Sample " + str(CurrentWell)) - # ctx.comment("Vol Height = "+str(round(RSBHeight,2))) - HeightDrop = DilutionVol / PiR2 - # ctx.comment("Vol Drop = "+str(round(HeightDrop,2))) - - if DilutionVol <= 0: - # If the No Volume - ctx.comment("Conc. Too Low, Skipping") - - elif DilutionVol >= MaxTubeVol - InitialVol: - # If the Required Dilution volume is >= Max Volume - DilutionVol = MaxTubeVol - InitialVol - ctx.comment( - "Conc. Too High, Will add, " - + str(DilutionVol) - + "ul, Max = " - + str(MaxTubeVol) - + "ul" - ) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - # ctx.comment("New Vol Height = "+str(round(RSBHeight,2))) - p1000.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(0.5) - ) # original = () - p1000.aspirate(100) - p1000.dispense(100) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].center()) - p1000.dispense(100) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - Mix += 1 - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - - else: - if DilutionVol <= 20: - # If the Required Dilution volume is <= 20ul - ctx.comment("Using p50 to add " + str(round(DilutionVol, 1))) - p50.pick_up_tip() - if round(float(data[current][3]), 1) <= 20: - p50.require_liquid_presence(RSB) - p50.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - else: - p50.require_liquid_presence(RSB) - p50.aspirate(20, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - p50.dispense(DilutionVol, sample_plate.wells_by_name()[CurrentWell]) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - p50.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - # Mix volume <=20ul - if DilutionVol + InitialVol <= 20: - p50.mix(10, DilutionVol + InitialVol) - elif DilutionVol + InitialVol > 20: - p50.mix(10, 20) - p50.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - ctx.delay(seconds=3) - p50.blow_out() - p50.drop_tip() if DRYRUN is False else p50.return_tip() - - elif DilutionVol > 20: - # If the required volume is >20 - ctx.comment("Using p1000 to add " + str(round(DilutionVol, 1))) - p1000.pick_up_tip() - p1000.require_liquid_presence(RSB) - p1000.aspirate(DilutionVol, RSB.bottom(RSBHeight - (HeightDrop))) - RSBHeight -= HeightDrop - if DilutionVol + InitialVol >= 120: - HighVolMix = 10 - for Mix in range(HighVolMix): - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.aspirate(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom( - z=dot_bottom - ) - ) # original = () - p1000.aspirate(DilutionVol + InitialVol - 100) - p1000.dispense(100) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].center() - ) - p1000.dispense(DilutionVol + InitialVol - 100) - Mix += 1 - wells_with_liquids.append( - sample_plate.wells_by_name()[CurrentWell] - ) - else: - p1000.dispense( - DilutionVol, sample_plate.wells_by_name()[CurrentWell] - ) - p1000.move_to( - sample_plate.wells_by_name()[CurrentWell].bottom(z=dot_bottom) - ) # original = () - p1000.mix(10, DilutionVol + InitialVol) - p1000.move_to(sample_plate.wells_by_name()[CurrentWell].top()) - wells_with_liquids.append(sample_plate.wells_by_name()[CurrentWell]) - ctx.delay(seconds=3) - p1000.blow_out() - p1000.drop_tip() if DRYRUN is False else p1000.return_tip() - current += 1 - - ctx.comment("==============================================") - ctx.comment("Results") - ctx.comment("==============================================") - - current = 1 - while current < len(data): - - CurrentWell = str(data[current][1]) - if float(data[current][2]) > 0: - InitialVol = float(data[current][2]) - else: - InitialVol = 0 - if float(data[current][3]) > 0: - InitialConc = float(data[current][3]) - else: - InitialConc = 0 - if float(data[current][4]) > 0: - TargetConc = float(data[current][4]) - else: - TargetConc = 0 - TotalDNA = float(InitialConc * InitialVol) - if TargetConc > 0: - TargetVol = float(TotalDNA / TargetConc) - else: - TargetVol = InitialVol - if TargetVol > InitialVol: - DilutionVol = float(TargetVol - InitialVol) - else: - DilutionVol = 0 - if DilutionVol > MaxTubeVol - InitialVol: - DilutionVol = MaxTubeVol - InitialVol - FinalVol = float(DilutionVol + InitialVol) - if TotalDNA > 0 and FinalVol > 0: - FinalConc = float(TotalDNA / FinalVol) - else: - FinalConc = 0 - ctx.comment( - "Sample " - + CurrentWell - + ": " - + str(round(FinalVol, 1)) - + " at " - + str(round(FinalConc, 1)) - + "ng/ul" - ) - - current += 1 - helpers.find_liquid_height_of_all_wells(ctx, p50, wells_with_liquids) diff --git a/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py new file mode 100644 index 00000000000..66db85468f4 --- /dev/null +++ b/abr-testing/abr_testing/protocols/active_protocols/3_Tartrazine Protocol.py @@ -0,0 +1,155 @@ +"""Tartrazine Protocol.""" +from opentrons.protocol_api import ProtocolContext, ParameterContext, Well +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ( + AbsorbanceReaderContext, + HeaterShakerContext, +) +from datetime import datetime +from typing import Dict, List +import statistics + +metadata = { + "protocolName": "Tartrazine Protocol", + "author": "Opentrons ", + "source": "Protocol Library", +} + +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add parameters.""" + parameters.add_int( + variable_name="number_of_plates", + display_name="Number of Plates", + default=4, + minimum=1, + maximum=4, + ) + + +def run(ctx: ProtocolContext) -> None: + """Protocol.""" + number_of_plates = ctx.params.number_of_plates # type: ignore [attr-defined] + # Plate Reader + plate_reader: AbsorbanceReaderContext = ctx.load_module( + helpers.abs_mod_str, "A3" + ) # type: ignore[assignment] + hs: HeaterShakerContext = ctx.load_module(helpers.hs_str, "A1") # type: ignore[assignment] + hs_adapter = hs.load_adapter("opentrons_universal_flat_adapter") + tube_rack = ctx.load_labware( + "opentrons_10_tuberack_nest_4x50ml_6x15ml_conical", "C2", "Reagent Tube" + ) + tartrazine_tube = tube_rack["A3"] + water_tube_1 = tube_rack["A4"] + water_tube_2 = tube_rack["B3"] + sample_plate_1 = ctx.load_labware( + "corning_96_wellplate_360ul_flat", "D1", "Sample Plate 1" + ) + sample_plate_2 = ctx.load_labware( + "corning_96_wellplate_360ul_flat", "D2", "Sample Plate 2" + ) + sample_plate_3 = ctx.load_labware( + "corning_96_wellplate_360ul_flat", "C1", "Sample Plate 3" + ) + sample_plate_4 = ctx.load_labware( + "corning_96_wellplate_360ul_flat", "B1", "Sample Plate 4" + ) + + sample_plate_list = [sample_plate_1, sample_plate_2, sample_plate_3, sample_plate_4] + tiprack_50_1 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "D3") + tiprack_50_2 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "C3") + tiprack_50_3 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B3") + tiprack_1000_1 = ctx.load_labware("opentrons_flex_96_tiprack_1000ul", "A2") + tip_racks = [tiprack_50_1, tiprack_50_2, tiprack_50_3] + + # Pipette + p50 = ctx.load_instrument("flex_1channel_50", "left", tip_racks=tip_racks) + p1000 = ctx.load_instrument( + "flex_1channel_1000", "right", tip_racks=[tiprack_1000_1] + ) + + # Probe wells + liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { + "Tartrazine": [{"well": tartrazine_tube, "volume": 45.0}], + "Water": [{"well": [water_tube_1, water_tube_2], "volume": 45.0}], + } + helpers.find_liquid_height_of_loaded_liquids(ctx, liquid_vols_and_wells, p50) + + i = 0 + all_percent_error_dict = {} + cv_dict = {} + vol = 0.0 + tip_count = 0 + for sample_plate in sample_plate_list[:number_of_plates]: + deck_locations = ["D1", "D2", "C1", "B1"] + p1000.pick_up_tip() + for well in sample_plate.wells(): + if vol < 45000: + tube_of_choice = water_tube_1 + else: + tube_of_choice = water_tube_2 + p50.pick_up_tip() + p1000.aspirate(190, tube_of_choice) + p1000.air_gap(5) + p1000.dispense(5, well.top()) + p1000.dispense(190, well) + vol += 190 + height = helpers.find_liquid_height(p50, tartrazine_tube) + p50.aspirate(10, tartrazine_tube.bottom(z=height)) + p50.air_gap(5) + p50.dispense(5, well.top()) + p50.dispense(10, well.bottom(z=0.5)) + p50.blow_out() + p50.return_tip() + tip_count += 1 + if tip_count >= (96 * 3): + p50.reset_tipracks() + p1000.return_tip() + helpers.move_labware_to_hs(ctx, sample_plate, hs, hs_adapter) + helpers.set_hs_speed(ctx, hs, 1500, 2.0, True) + hs.open_labware_latch() + plate_reader.close_lid() + plate_reader.initialize("single", [450]) + plate_reader.open_lid() + ctx.move_labware(sample_plate, plate_reader, use_gripper=True) + sample_plate_name = "sample plate_" + str(i + 1) + csv_string = sample_plate_name + "_" + str(datetime.now()) + plate_reader.close_lid() + result = plate_reader.read(csv_string) + for wavelength in result: + dict_of_wells = result[wavelength] + readings_and_wells = dict_of_wells.items() + readings = dict_of_wells.values() + avg = statistics.mean(readings) + # Check if every average is within +/- 5% of 2.85 + percent_error_dict = {} + percent_error_sum = 0.0 + for reading in readings_and_wells: + well_name = str(reading[0]) + measurement = reading[1] + percent_error = (measurement - 2.85) / 2.85 * 100 + percent_error_dict[well_name] = percent_error + percent_error_sum += percent_error + avg_percent_error = percent_error_sum / 96.0 + standard_deviation = statistics.stdev(readings) + try: + cv = standard_deviation / avg + except ZeroDivisionError: + cv = 0.0 + cv_percent = cv * 100 + cv_dict[sample_plate_name] = { + "CV": cv_percent, + "Mean": avg, + "SD": standard_deviation, + "Avg Percent Error": avg_percent_error, + } + all_percent_error_dict[sample_plate_name] = percent_error_dict + plate_reader.open_lid() + ctx.move_labware(sample_plate, deck_locations[i], use_gripper=True) + i += 1 + # Print percent error dictionary + ctx.comment("Percent Error: " + str(all_percent_error_dict)) + # Print cv dictionary + ctx.comment("Plate Reader result: " + str(cv_dict)) diff --git a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py index 2e835ac04dd..4894cae41d4 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py +++ b/abr-testing/abr_testing/protocols/active_protocols/8_Illumina and Plate Reader.py @@ -19,10 +19,7 @@ } -requirements = { - "robotType": "Flex", - "apiLevel": "2.21", -} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} HELLMA_PLATE_SLOT = "D4" PLATE_READER_SLOT = "C3" @@ -58,14 +55,11 @@ def add_parameters(parameters: ParameterContext) -> None: """Add Parameters.""" helpers.create_hs_speed_parameter(parameters) helpers.create_dot_bottom_parameter(parameters) - parameters.add_str( + parameters.add_bool( variable_name="plate_orientation", display_name="Hellma Plate Orientation", - default="0_deg", - choices=[ - {"display_name": "0 degree Rotation", "value": "0_deg"}, - {"display_name": "180 degree Rotation", "value": "180_deg"}, - ], + default=True, + description="If hellma plate is rotated, set to True.", ) @@ -73,6 +67,7 @@ def plate_reader_actions( protocol: ProtocolContext, plate_reader: AbsorbanceReaderContext, hellma_plate: Labware, + hellma_plate_name: str, ) -> None: """Plate reader single and multi wavelength readings.""" wavelengths = [450, 650] @@ -84,7 +79,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -95,7 +90,7 @@ def plate_reader_actions( protocol.move_labware(hellma_plate, plate_reader, use_gripper=True) plate_reader.close_lid() result = plate_reader.read(str(datetime.now())) - msg = f"result: {result}" + msg = f"{hellma_plate_name} result: {result}" protocol.comment(msg=msg) plate_reader.open_lid() protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True) @@ -107,6 +102,8 @@ def run(protocol: ProtocolContext) -> None: # LOAD PARAMETERS heater_shaker_speed = protocol.params.heater_shaker_speed # type: ignore[attr-defined] dot_bottom = protocol.params.dot_bottom # type: ignore[attr-defined] + plate_orientation = protocol.params.plate_orientation # type: ignore[attr-defined] + plate_name_str = "hellma_plate_" + str(plate_orientation) global p200_tips global p50_tips # WASTE BIN @@ -182,7 +179,7 @@ def run(protocol: ProtocolContext) -> None: PPC = reagent_plate.wells_by_name()["A6"] EPM = reagent_plate.wells_by_name()["A7"] # Load Liquids - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) # tip and sample tracking if COLUMNS == 1: @@ -948,4 +945,4 @@ def tipcheck() -> None: p1000.return_tip() if TIP_TRASH is False else p1000.drop_tip() p200_tips += 1 tipcheck() - plate_reader_actions(protocol, plate_reader, hellma_plate) + plate_reader_actions(protocol, plate_reader, hellma_plate, plate_name_str) diff --git a/abr-testing/abr_testing/protocols/csv_parameters/3_samplevols.csv b/abr-testing/abr_testing/protocols/csv_parameters/3_samplevols.csv deleted file mode 100644 index bc952b330a1..00000000000 --- a/abr-testing/abr_testing/protocols/csv_parameters/3_samplevols.csv +++ /dev/null @@ -1,25 +0,0 @@ -Sample_Plate, Sample_well,InitialVol,InitialConc,TargetConc -sample_plate,A2,10,3.94,1 -sample_plate,B2,10,3.5,1 -sample_plate,C2,10,3.46,1 -sample_plate,D2,10,3.1,1 -sample_plate,E2,10,2.64,1 -sample_plate,F2,10,3.16,1 -sample_plate,G2,10,2.9,1 -sample_plate,H2,10,2.8,1 -sample_plate,A3,10,2.82,1 -sample_plate,B3,10,2.84,1 -sample_plate,C3,10,2.72,1 -sample_plate,D3,10,2.9,1 -sample_plate,A5,10,3.94,1 -sample_plate,B5,10,3.5,1 -sample_plate,C5,10,3.46,1 -sample_plate,D5,10,3.1,1 -sample_plate,E5,10,2.64,1 -sample_plate,F5,10,3.16,1 -sample_plate,G5,10,2.9,1 -sample_plate,H5,10,2.8,1 -sample_plate,A6,10,2.82,1 -sample_plate,B6,10,2.84,1 -sample_plate,C6,10,2.72,1 -sample_plate,D6,10,2.9,1 diff --git a/abr-testing/abr_testing/protocols/liquid_setups/3_OT3 ABR Normalize with Tubes Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py similarity index 73% rename from abr-testing/abr_testing/protocols/liquid_setups/3_OT3 ABR Normalize with Tubes Liquid Setup.py rename to abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py index 86e4de2aeed..9e0b29a03ed 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/3_OT3 ABR Normalize with Tubes Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/3_Tartrazine Liquid Setup.py @@ -1,11 +1,11 @@ -"""Plate Filler Protocol for Simple Normalize Long.""" +"""Plate Filler Protocol for Tartrazine Protocol.""" from opentrons import protocol_api from abr_testing.protocols.helpers import ( load_common_liquid_setup_labware_and_instruments, ) metadata = { - "protocolName": "DVT1ABR3 Liquids: Flex Normalize with Tubes", + "protocolName": "DVT1ABR3 Liquids: Tartrazine Protocol", "author": "Rhyann clarke ", "source": "Protocol Library", } @@ -32,9 +32,16 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Transfer Liquid p1000.transfer( - 4000, + 45000, source_reservoir["A1"], - reagent_tube["A1"].top(), + reagent_tube["B3"].top(), + blowout=True, + blowout_location="source well", + ) + p1000.transfer( + 45000, + source_reservoir["A1"], + reagent_tube["A4"].top(), blowout=True, blowout_location="source well", ) diff --git a/abr-testing/abr_testing/tools/make_push.py b/abr-testing/abr_testing/tools/make_push.py new file mode 100644 index 00000000000..28a69b11103 --- /dev/null +++ b/abr-testing/abr_testing/tools/make_push.py @@ -0,0 +1,95 @@ +"""Push one or more folders to one or more robots.""" +import subprocess +import multiprocessing +import json + +global folders +# Opentrons folders that can be pushed to robot +folders = [ + "abr-testing", + "hardware-testing", + "abr-testing + hardware-testing", + "other", +] + + +def push_subroutine(cmd: str) -> None: + """Pushes specified folder to specified robot.""" + try: + subprocess.run(cmd) + except Exception: + print("failed to push folder") + raise + + +def main(folder_to_push: str, robot_to_push: str) -> int: + """Main process!""" + cmd = "make -C {folder} push-ot3 host={ip}" + robot_ip_path = "" + push_cmd = "" + folder_int = int(folder_to_push) + if folders[folder_int].lower() == "abr-testing + hardware-testing": + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips_dict = robot_json.get("ip_address_list") + robot_ips = list(robot_ips_dict.keys()) + ip_file.close() + else: + robot_ips = [robot_to_push] + for folder_name in folders[:-2]: + # Push abr-testing and hardware-testing folders to all robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process( + target=push_subroutine, args=(push_cmd,) + ) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + else: + + if folder_int == (len(folders) - 1): + folder_name = input("Which folder? ") + else: + folder_name = folders[folder_int] + if robot_to_push.lower() == "all": + robot_ip_path = input("Path to robot ips: ") + with open(robot_ip_path, "r") as ip_file: + robot_json = json.load(ip_file) + robot_ips = robot_json.get("ip_address_list") + ip_file.close() + else: + robot_ips = [robot_to_push] + + # Push folder to robots + for robot in robot_ips: + print_proc = multiprocessing.Process( + target=print, args=(f"Pushing {folder_name} to {robot}!\n\n",) + ) + print_proc.start() + print_proc.join() + push_cmd = cmd.format(folder=folder_name, ip=robot) + process = multiprocessing.Process(target=push_subroutine, args=(push_cmd,)) + process.start() + process.join() + print_proc = multiprocessing.Process(target=print, args=("Done!\n\n",)) + print_proc.start() + print_proc.join() + return 0 + + +if __name__ == "__main__": + for i, folder in enumerate(folders): + print(f"{i}) {folder}") + folder_to_push = input("Please Select a Folder to Push: ") + robot_to_push = input("Type in robots ip (type all for all): ") + print(main(folder_to_push, robot_to_push)) diff --git a/analyses-snapshot-testing/Makefile b/analyses-snapshot-testing/Makefile index de5e0381131..6918d17bf3e 100644 --- a/analyses-snapshot-testing/Makefile +++ b/analyses-snapshot-testing/Makefile @@ -3,12 +3,12 @@ CACHEBUST ?= $(shell date +%s) ANALYSIS_REF ?= edge PROTOCOL_NAMES ?= all OVERRIDE_PROTOCOL_NAMES ?= all -OPENTRONS_VERSION ?= edge +LOCAL_IMAGE_TAG ?= local +ANALYZER_IMAGE_NAME ?= opentrons-analysis -export OPENTRONS_VERSION # used for server -export ANALYSIS_REF # used for analysis and snapshot test -export PROTOCOL_NAMES # used for the snapshot test -export OVERRIDE_PROTOCOL_NAMES # used for the snapshot test +export ANALYSIS_REF # tag, branch or commit for the opentrons repository. Used as the image tag for the analyzer image +export PROTOCOL_NAMES # tell the test which protocols to run +export OVERRIDE_PROTOCOL_NAMES # tell the test which override protocols to run ifeq ($(CI), true) PYTHON=python @@ -93,23 +93,47 @@ build-base-image: .PHONY: build-opentrons-analysis build-opentrons-analysis: - @echo "Building docker image for $(ANALYSIS_REF)" - @echo "The image will be named opentrons-analysis:$(ANALYSIS_REF)" - @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. + @echo "Building docker image for opentrons repository reference$(ANALYSIS_REF)" + @echo "The image will be named $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF)" + @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t $(ANALYZER_IMAGE_NAME):$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. -.PHONY: local-build -local-build: +.PHONY: build-local +build-local: @echo "Building docker image for your local opentrons code" - @echo "The image will be named opentrons-analysis:local" - @echo "For a fresh build, run 'make local-build NO_CACHE=1'" - docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) $(BUILD_FLAGS) -t opentrons-analysis:local -f citools/Dockerfile.local .. || true + @echo "This image will be named $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -t $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG) -f citools/Dockerfile.local .. @echo "Build complete" +.PHONY: snapshot-test-local +snapshot-test-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv + +.PHONY: snapshot-test-update-local +snapshot-test-update-local: ANALYSIS_REF=$(LOCAL_IMAGE_TAG) +snapshot-test-update-local: build-base-image build-local + @echo "This target is overriding the ANALYSIS_REF to the LOCAL_IMAGE_TAG: $(LOCAL_IMAGE_TAG)" + @echo "ANALYSIS_REF is $(ANALYSIS_REF). The the test maps this env variable to the image tag." + @echo "The image the test will use is $(ANALYZER_IMAGE_NAME):$(LOCAL_IMAGE_TAG)" + @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" + @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update + .PHONY: generate-protocols generate-protocols: $(PYTHON) -m pipenv run python -m automation.data.protocol_registry +# Tools for running the robot server in a container + +OPENTRONS_VERSION ?= edge +export OPENTRONS_VERSION # used for the robot server image as the tag, branch or commit for the opentrons repository + .PHONY: build-rs build-rs: @echo "Building docker image for opentrons-robot-server:$(OPENTRONS_VERSION)" diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 78423b8447f..03ce1d87518 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -4,7 +4,7 @@ 1. Follow the instructions in [DEV_SETUP.md](../DEV_SETUP.md) 1. `cd analyses-snapshot-testing` -1. use pyenv to install python 3.12 and set it as the local python version for this directory +1. use pyenv to install python 3.13 and set it as the local python version for this directory 1. `make setup` 1. Have docker installed and ready @@ -72,10 +72,17 @@ cd analyses-snapshot-testing \ > This copies in your local code to the container and runs the analyses battery against it. -1. `make build-base-image` -1. `make build-local` -1. `make local-snapshot-test` +`cd PYENV_ROOT && git pull` - make sure pyenv is up to date so you may install python 3.13.0 +`pyenv install 3.13.0` - install python 3.13.0 +`cd /analyses-snapshot-testing` - navigate to the analyses-snapshot-testing directory +`pyenv local 3.13.0` - set the local python version to 3.13.0 +`make setup` - install the requirements +`make snapshot-test-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created You have the option to specify one or many protocols to run the analyses on. This is also described above [Running the tests against specific protocols](#running-the-tests-against-specific-protocols) -- `make local-snapshot-test PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` +- `make snapshot-test-local PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` + +### Updating the snapshots locally + +- `make snapshot-test-update-local` - this target builds the base image, builds the local code into the base image, then runs the analyses battery against the image you just created, updating the snapshots by passing the `--update-snapshots` flag to the test diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json index 3d8b4b072eb..f59c9684e23 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[004ebb2b82][OT2_S_v2_11_P10S_P300M_MM_TC1_TM_Swift].json @@ -16668,6 +16668,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json index ddb334a58e0..05fa920a764 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[00574c503f][pl_BacteriaInoculation_Flex_6plates].json @@ -41584,6 +41584,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "Bacterial culture medium (e.g., LB broth)", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json index 38872b09ff8..c709366a42a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[011481812b][OT2_S_v2_7_P20S_None_Walkthrough].json @@ -4919,6 +4919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json index aac975221e8..2da7a9c47bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[01255c3f3b][pl_Flex_Protein_Digestion_Protocol].json @@ -11824,6 +11824,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json index ff626992e43..4f1452dcdfc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0190369ce5][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1NoFixtures].json @@ -11452,6 +11452,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json index 8cd99860d7e..39491fae6aa 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0256665840][OT2_S_v2_16_P300M_P20S_aspirateDispenseMix0Volume].json @@ -2917,6 +2917,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json index c62ceb23edd..a561da0a387 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[041ad55e7b][OT2_S_v2_15_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3113,6 +3113,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json index a2aca7e252a..fe3d81be11b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[057de2035d][OT2_S_v2_19_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json index bce38cbe476..f85b03c5703 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0903a95825][Flex_S_v2_19_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json index 2ca289680ef..0f7d7d308b5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09676b9f7e][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 7a7269decb6..d9895fb2c9e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[09ba51132a][OT2_S_v2_14_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json index 4891466d0b7..f892fc456ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0a9ef592c8][Flex_S_v2_18_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json index 64072eb8834..a877268d0bd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0affe60373][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json index dfef8b35364..3a0f63a8f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0b42cfc151][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_row].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 0096a483ffe..957e685c737 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0c4ae179bb][OT2_S_v2_15_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17072,6 +17072,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json index e4924262e1a..35dc7ecc804 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0cbde10c37][OT2_S_v2_18_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -9569,6 +9569,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json index 7bff37154bf..db42ce35fdc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0dd21c0ee5][pl_EM_seq_48Samples_AllSteps_Edits_150].json @@ -49392,6 +49392,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json index d0b11f42740..cf56c96470e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[109b7ad1f0][Flex_S_v2_20_96_None_ROW_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a ROW partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json index f7457a3c48d..e4de2f89a14 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[10c2386b92][Flex_S_v2_20_96_AllCorners].json @@ -33697,6 +33697,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json index 6fb9e302070..7ce2978d56a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[11020a4e17][pl_Bradford_proteinassay].json @@ -19352,6 +19352,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Coomassie Brilliant Blue G-250 solution ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json index f2c63721b33..d33b6cf51cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[134037b2aa][OT2_X_v6_P300M_P20S_HS_MM_TM_TC_AllMods].json @@ -7997,6 +7997,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json index c463feb0552..8fece97c06c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13e5a9c68b][Flex_S_v2_20_P8X1000_P50_LLD].json @@ -6203,6 +6203,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "water for ER testing", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json index 0b2e524dee6..c30b18aa93e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[13ec753045][Flex_S_v2_18_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json index 6053323ac4b..3f500210e5a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[15a60fccf4][pl_microBioID_beads_touchtip].json @@ -34590,6 +34590,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json index ababd25acfa..059f375baec 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[194e3c49bb][pl_Normalization_with_PCR].json @@ -9297,6 +9297,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Rami Farawi ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json index 858286887b6..fd7b30ca845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4b9e0f93d9][OT2_S_v2_20_8_None_PARTIAL_COLUMN_HappyPathMixedTipRacks].json @@ -19714,6 +19714,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT-2 protocol with 1ch and 8ch pipette partial/single tip configurations. Mixing tipracks and using separate tipracks. ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json index d810bd75c88..b63443781ac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4cb705bdbf][Flex_X_v2_16_NO_PIPETTES_MM_MagneticModuleInFlexProtocol].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index 90bfa119fb7..a126374479b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4ea9d66206][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json index 3af042768f6..f102cab8bc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4f50c02c81][Flex_S_v2_19_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json index 843078fa552..484c6600849 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4fadc166c0][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_variable_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json index dfc888c15b5..3a839b9cdbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[50d7be4518][pl_Zymo_Quick_RNA_Cells_Flex_multi].json @@ -23420,6 +23420,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cells in DNA/ RNA Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json index d2955132ff2..72f8481bc29 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51a761307d][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultOutOfRangeRTP_Override_default_greater_than_maximum].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json index 2b447932025..4c45089bc7c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[51fc977577][OT2_S_v6_P300M_P20S_MixTransferManyLiquids].json @@ -6064,6 +6064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index 0aaa562c15c..58b6e3ffb42 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[53db9bf516][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -1284,6 +1284,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json index 952985449d9..dde453f20ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[549cc904ac][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_c3_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json index d28023877a0..8cb5125c17c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54b0b509cd][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_no_end].json @@ -2395,6 +2395,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json index 9cad51f6d80..27656b80cca 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[54f717cfd1][OT2_S_v2_16_P300S_None_verifyNoFloatingPointErrorInPipetting].json @@ -1894,6 +1894,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json index c5c5f1a2e67..84bff8651d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[555b2fff00][Flex_S_v2_19_Illumina_DNA_Prep_48x].json @@ -49707,6 +49707,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json index 7c04e4274de..63ed50d9c04 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[58750bf5fb][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json index 3646ae2d522..4744b1f1992 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[59b00713a7][Flex_S_v2_18_ligseq].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json index e608af8c173..353a1b46f45 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5c57add326][pl_Omega_HDQ_DNA_Bacteria_Flex_96_channel].json @@ -46193,6 +46193,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Resuspended in PBS", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json index c76b2aca7f9..059e7fc2b84 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5e958b7c98][Flex_X_v2_16_P300MGen2_None_OT2PipetteInFlexProtocol].json @@ -1258,6 +1258,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json index a107fa87e60..47c65a0dfc5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5edb9b4de3][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_2].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json index 86d3274f412..3c69dda38e7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60015c6e65][OT2_X_v2_18_None_None_duplicateRTPVariableName].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Multiple RTP Variables with Same Name" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json index 0de0eff0022..fde783d94b8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[604023f7f1][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol3].json @@ -197,6 +197,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json index 726906c04d4..b8ef1cbc5f2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[60c1d39463][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_int_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json index 1dcac6e453a..180178d1d44 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6122c72996][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_1].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json index d8409d8db46..8623a021746 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6126498df7][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInStagingAreaCol4].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json index afa5bb0b4d2..8b06eca9390 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[61619d5498][Flex_S_v2_18_NO_PIPETTES_GoldenRTP].json @@ -295,6 +295,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Golden RTP Examples" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json index 385da3c78a4..b1528f23cbf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[618f29898f][pl_Flex_customizable_serial_dilution_upload].json @@ -10386,6 +10386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Diluent liquid is filled in the reservoir", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json index 5681dc28194..1441d3d1cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[63ea171b47][pl_M_N_Nucleomag_DNA_Flex_multi].json @@ -16386,6 +16386,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Cell Pellet", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json index 1bf35620512..d27c90a866c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[675d2c2562][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json index b2ec113fe4e..7209e028a2b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[68614da0b3][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_east].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json index e545da56bd4..e30b5bee0d8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6ad5590adf][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json index 19ac0d4e0f7..3ac36a59ee5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6c20d6c570][Flex_S_v2_20_96_None_COLUMN_HappyPath].json @@ -6263,6 +6263,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "96 channel pipette and a COLUMN partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json index 8f88134625a..da1993d6e56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6cee20a957][OT2_S_v2_12_NO_PIPETTES_Python310SyntaxRobotAnalysisOnlyError].json @@ -94,6 +94,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json index cf3e8bf4aa3..0e079e7daa2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e2f6f10c5][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_destination_collision].json @@ -3946,6 +3946,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json index 66877246558..eba57a84196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e34343cfc][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TM_MagMaxRNAExtraction].json @@ -56935,6 +56935,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json index 63567ca7c96..f052823d867 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e5128f107][OT2_X_v2_16_None_None_HS_HeaterShakerConflictWithTrashBin1].json @@ -512,6 +512,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Heater-shaker conflict OT-2" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json index cae3345ff13..2b5614762ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6e744cbb48][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_str_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json index d67ff04865b..fd1c3550795 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f246e1cd8][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAEnrichmentV4].json @@ -45393,6 +45393,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json index 80a9f7d117a..0028c36df1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f3e297a11][OT2_S_v2_3_P300S_None_MM1_MM2_TM_Mix].json @@ -3435,6 +3435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.3" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json index ad8638a9e6d..86023eb8c12 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[6f84e60cb0][OT2_S_v2_16_P300M_P20S_HS_TC_TM_aspirateDispenseMix0Volume].json @@ -2849,6 +2849,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json index cbd7839e9ad..b79aec33a1b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[70b873c24b][pl_SamplePrep_MS_Digest_Flex_upto96].json @@ -65064,6 +65064,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "100 mM ABC in MS grade water, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json index 1b70c59e4b6..a79c72e6781 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[733c9cdf62][Flex_S_v2_20_8_None_PARTIAL_COLUMN_HappyPath].json @@ -5042,6 +5042,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json index 30ddffb8e03..3a2911f043d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7583faec7c][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json index bc24730fad8..c577f539508 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[770ebdcd29][pl_KAPA_Library_Quant_48_v8].json @@ -31929,6 +31929,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Dilution Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json index 19cf70d2edb..00efc4b9178 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[79e61426a2][Flex_S_v2_18_AMPure_XP_48x].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json index bd4f009a701..ae9e8d99862 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7d06568bfe][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json index df37cc2db4b..48077d59118 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7e7a90041b][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_west_column].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json index 47ce454e920..ac4311409dc 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ebefe4580][pl_QIAseq_FX_24x_Normalizer_Workflow_B].json @@ -75905,6 +75905,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json index 0c559ae74b3..235d5eb9fe3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7f2ef0eaff][Flex_X_v2_18_NO_PIPETTES_Overrides_DefaultChoiceNoMatchChoice_Override_float_default_no_matching_choices].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "default choice does not match a choice" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json index 44584111a12..e0fd663c213 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[82e9853b34][Flex_X_v2_16_NO_PIPETTES_TrashBinInCol2].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json index 63aed19f5f3..d4cf07c0f99 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8455adcea9][OT2_S_v2_12_P300M_P20S_FailOnRun].json @@ -2666,6 +2666,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.12", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json index 80ce54abbcb..2312c3a011e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[84f684cbc4][Flex_S_v2_18_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 9f3a0d8a1fb..ac524674f7e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8860ee702c][OT2_S_v2_14_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14372,6 +14372,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json index 3cc6db1a5cd..7fb0dceab92 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88abcfdbca][pl_Zymo_Quick_RNA_Cells_Flex_96_Channel].json @@ -49169,6 +49169,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Sample Volume in Shield", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json index 5d219d91f72..7eedccb2cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[88de2fb78f][OT2_X_v6_P20S_P300M_HS_HSCollision].json @@ -5373,6 +5373,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json index 2b9cd2584d3..70bb212b45b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a255db0b][OT2_X_v2_18_None_None_StrRTPwith_unit].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Str RTP with unit" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json index 47511dff64f..ea3c1cc76b0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[89a8226c4e][Flex_X_v2_16_P1000_96_TC_PartialTipPickupThermocyclerLidConflict].json @@ -4997,6 +4997,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json index e2fadc01642..cadf197c142 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8a663305c4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_south].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index 919f1980537..e433acf53ff 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8b07e799f6][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json index b8dd13f5f42..c21c19205cf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8e1f35ed6c][pl_NiNTA_Flex_96well_PlatePrep_final].json @@ -18477,6 +18477,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Equilibration Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json index 1d83bf0706f..933aa66cf7d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8fcfd2ced0][Flex_S_v2_16_P1000_96_TC_PartialTipPickupColumn].json @@ -3727,6 +3727,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json index 349fbd62034..794499f75ce 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[918747b2f9][pl_Illumina_DNA_Prep_48x_v8].json @@ -52167,6 +52167,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 2c3d142321b..d9f59af3587 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[93b724671e][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -2468,6 +2468,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json index 8c086d8fdff..405df785df9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[94913d2988][OT2_S_v3_P300SGen1_None_Gen1PipetteSimple].json @@ -6474,6 +6474,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json index 85ee931590d..004f5251126 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[95da6fbef2][Flex_S_2_15_P1000M_GRIP_HS_TM_MB_OmegaHDQDNAExtractionBacteria].json @@ -32273,6 +32273,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json index cbad73a3a2d..8a871949e46 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9618a6623c][OT2_X_v2_11_P300S_TC1_TC2_ThermocyclerMoamError].json @@ -2775,6 +2775,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.11" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index cbf301b89e7..5538166da59 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[973fa979e6][Flex_S_v2_16_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json index b0f0b8ac0bd..23fd7f389a0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9bcb0a3f13][pl_normalization_with_csv].json @@ -6024,6 +6024,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json index 8d4e3a960dd..aba00388845 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[9e56ee92f6][Flex_X_v2_16_P1000_96_GRIP_DropLabwareIntoTrashBin].json @@ -1435,6 +1435,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json index 5a508d84d58..7cb88cd0308 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a01a35c14a][Flex_X_v2_16_NO_PIPETTES_TrashBinInStagingAreaCol3].json @@ -153,6 +153,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json index 7808bbc2d03..e2a5dced311 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a06502b2dc][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_description].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json index f951219fdff..5bc309d3cac 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a08c261369][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModulesNoFixtures].json @@ -9575,6 +9575,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json index eca34fc28c3..68185db5dbd 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0b755a1a1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north_west].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json index 965ca7d3ead..7aecea25f6a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a0dad2eb8e][pl_SamplePrep_MS_Cleanup_Flex_upto96].json @@ -50265,6 +50265,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Digested Protein samples, volume per well", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json index 1c3e57b481a..36400ae7de7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a3dfca7f0c][Flex_S_v2_19_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json index 42781ff6ea1..fe8184c0608 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a437534569][Flex_S_v2_19_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json index be8c1a00d13..e623aec42f7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a46928c103][pl_Nanopore_Genomic_Ligation_v5_Final].json @@ -22844,6 +22844,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json index 8a0a8a6a2ee..0aacb0b3e73 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a4d3b3a2d3][pl_96_ch_demo_rtp_with_hs].json @@ -16593,6 +16593,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "generic", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 321a04e20ac..e6cb5eace9a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a66d700ed6][OT2_S_v2_13_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -14092,6 +14092,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json index f4e89bf46a3..b0eb2e93f00 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8528e52b4][Flex_S_v2_20_96_None_SINGLE_HappyPathSouthSide].json @@ -9431,6 +9431,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Unsafe protocol ❗❗❗❗❗❗❗❗❗❗❗ will collide with tube.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json index 6afef67d006..86a33113a16 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a8a5ad823d][pl_cherrypicking_flex_v3].json @@ -10373,6 +10373,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Krishna Soma ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json index 01ce458ff53..52120bd6dc3 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a94f22054f][Flex_S_v2_18_kapahyperplus].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json index 60a0f1c77a3..1d043a44952 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[a9557d762c][Flex_X_v2_16_NO_PIPETTES_AccessToFixedTrashProp].json @@ -55,6 +55,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json index 8e14d013357..93eff2447db 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[aa61eee0bf][pl_sigdx_part2].json @@ -46385,6 +46385,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json index b4324589435..ec2e77260d2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac5a46e74b][pl_langone_pt2_ribo].json @@ -30083,6 +30083,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "ATL4", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json index ef9acd1b1a3..30f2c70b0ea 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac886d7768][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IDTXgen96Part1to3].json @@ -15276,6 +15276,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json index c8389b97d75..1f453e29cf8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[acefe91275][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_left].json @@ -2438,6 +2438,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json index 3a44acf987c..4af69fce36b 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad627dcedf][OT2_S_v6_P300M_P20S_HS_Smoke620release].json @@ -9492,6 +9492,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json index d3338855040..ed08b660a33 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ad9184067d][Flex_X_v2_18_NO_PIPETTES_ReservedWord].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Default not in range" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json index f86080f047c..a18485392e9 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[adc0621263][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLid].json @@ -6240,6 +6240,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json index a5b5bdb65cc..9f12179d1e2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[afd5d372a9][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_return_tip_error].json @@ -3766,6 +3766,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json index 5b0df3b070c..b41b7117e24 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b0ce7dde5d][Flex_X_v2_16_P1000_96_TC_PartialTipPickupTryToReturnTip].json @@ -3706,6 +3706,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json index 4433e026fd1..6dffb02e16c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b48407ff98][pl_cherrypicking_csv_airgap].json @@ -28815,6 +28815,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json index 7005e6011ab..43f62a32282 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b777168ac1][Flex_S_v2_19_Illumina_Stranded_total_RNA_Ribo_Zero].json @@ -4492,6 +4492,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json index b3637624ed4..7956e369c52 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b79134ab8a][OT2_X_v2_20_8_Overrides_InvalidConfigs_Override_drop_tip_with_location].json @@ -4920,6 +4920,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json index 6b2391f6118..cf8ec946db5 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b806f07be9][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_choice_value].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json index 4bcec7cf7de..db24530e196 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[b91d31eaa2][pl_MagMax_RNA_Cells_Flex_96_Channel].json @@ -41229,6 +41229,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Magnetic Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json index 5460d2d1fd7..851fd7e1fbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[baf79d9b4a][Flex_S_v2_15_P1000S_None_SimpleNormalizeLongRight].json @@ -125769,6 +125769,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json index 290674f3bd6..8c741ed84ba 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[bc049301c1][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_transfer_destination_collision].json @@ -3912,6 +3912,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json index 67a07aa1297..8c38d6c5f57 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c0435f08da][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_north].json @@ -1481,6 +1481,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json index 14cc53aba17..4d2cd13d215 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c064d0de2c][OT2_S_v6_P1000S_None_SimpleTransfer].json @@ -1977,6 +1977,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index d6eb8a28124..c2e5c309dcf 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c195291f84][OT2_S_v2_17_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index f59969368ab..7c817b2b869 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c1c04baffd][Flex_S_v2_17_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 4c6c38162b3..f69446ee9cb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c3098303ad][OT2_S_v2_15_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -154,6 +154,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json index 4665f21b62e..fe3f96abc67 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c343dfb5a0][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_bottom_right_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json index aadd38b4eaa..1a48b84ae0a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c745e5824a][Flex_S_v2_16_P1000_96_GRIP_DeckConfiguration1NoModules].json @@ -12315,6 +12315,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json index 27e9d4f2c51..253a2bcff9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c821e64fad][OT2_S_v2_13_P300M_P20S_MM_TC_TM_Smoke620Release].json @@ -10777,6 +10777,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json index 131c7514649..06153dd11b4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c8d2ca0089][Flex_S_v2_18_QIASeq_FX_48x].json @@ -66156,6 +66156,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json index 5efbff81ebc..cf748ae6fa0 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[c9e6e3d59d][OT2_X_v4_P300M_P20S_MM_TC1_TM_e2eTests].json @@ -7184,6 +7184,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json index 1759b7b244f..ac74dbfc2a4 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cb5adc3d23][OT2_S_v6_P20S_P300M_TransferReTransferLiquid].json @@ -12796,6 +12796,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json index 4ad4434ab42..c9afc886f56 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cc26c104b4][Flex_S_v2_20_8_None_SINGLE_HappyPath].json @@ -7179,6 +7179,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "Tip Rack South Clearance for the 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json index 4e8f71a17c1..b591039cbbb 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cecd51c8ee][pl_ExpressPlex_96_final].json @@ -13346,6 +13346,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Index Plate color", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json index 1149640d8b1..f449eff0d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d0154b1493][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_eight_partial_column_bottom_right].json @@ -1314,6 +1314,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json index 6e02fa8a3f3..a4d46be3d94 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d14738bdfe][pl_Zymo_Magbead_DNA_Cells_Flex_multi].json @@ -18032,6 +18032,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Samples", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json index 9d35aba10fc..2f0c52a853f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d29d74d7fb][pl_QIASeq_FX_48x_v8].json @@ -70949,6 +70949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "CleanupBead Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json index 52e87c76f46..1b664b4e963 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d2c818bf00][Flex_S_v2_20_P50_LPD].json @@ -5113,6 +5113,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Test this wet!!!", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json index 6f5f1f09b83..7fd14d2f851 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d391213095][Flex_S_v2_15_P1000_96_GRIP_HS_TM_QuickZymoMagbeadRNAExtractionCellsOrBacteria].json @@ -25208,6 +25208,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Zach Galluzzo ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json index f0d2d744031..7916f424286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d3b28ea1d7][pl_Zymo_Magbead_DNA_Cells_Flex_96_channel].json @@ -40886,6 +40886,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Lysis Buffer", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json index 3056b873a74..718e0a0df13 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d48bc4f0c9][OT2_S_v2_17_P300M_P20S_HS_TC_TM_SmokeTestV3].json @@ -17430,6 +17430,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json index 026977dbcc6..5b6c3c3c690 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6026e11c5][OT2_X_v2_7_P300S_TwinningError].json @@ -2836,6 +2836,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.7", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json index 99ccd21cc19..170de395195 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d61739e6a3][Flex_S_v2_19_IDT_xGen_EZ_48x].json @@ -59919,6 +59919,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json index 6b342319f31..1ad848a9ef8 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d6389183c0][pl_AMPure_XP_48x_v8].json @@ -28213,6 +28213,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "AMPure Beads", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json index 7ea850030fd..87b61a0454d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d7e862d601][OT2_S_v2_18_None_None_duplicateChoiceValue].json @@ -43,6 +43,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Duplicate choice value" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json index 1e9b318abf5..aba7dd56957 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8cb88b3b2][Flex_S_v2_16_P1000_96_TC_PartialTipPickupSingle].json @@ -3591,6 +3591,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json index 65c2da26059..bd408636813 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d979799443][OT2_S_v2_20_8_None_SINGLE_HappyPath].json @@ -8241,6 +8241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "OT2 8 Channel pipette and a SINGLE partial tip configuration.", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json index 8b7cf7214ac..2651a003e75 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[da326082e1][pl_Hyperplus_tiptracking_V4_final].json @@ -29144,6 +29144,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index b262ea72c0f..c52ed516ba1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dabb7872d8][Flex_S_v2_19_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -13164,6 +13164,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json index beb0aa09c29..62ea1e316b2 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[db1fae41ec][Flex_X_v2_20_96_and_8_Overrides_InvalidConfigs_Override_ninety_six_partial_column_3].json @@ -1241,6 +1241,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json index 2288dccf926..9e3cf07280a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dbba7a71a8][OT2_S_v2_16_NO_PIPETTES_TC_VerifyThermocyclerLoadedSlots].json @@ -145,6 +145,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json index 0353b26aed1..2eb5308529f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[de4249ddfb][Flex_X_v2_16_NO_PIPETTES_TC_TrashBinAndThermocyclerConflict].json @@ -171,6 +171,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Thermocycler conflict 1" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json index b22e56cb8ed..1bb680c2c4f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e0b0133ffb][pl_Illumina_Stranded_total_RNA_Ribo_Zero_protocol].json @@ -39234,6 +39234,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Dandra Howell ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json index 6a1c9e67b51..6c6c30ace61 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e18bdd6f5d][Flex_S_2_15_P1000M_P50M_GRIP_HS_TM_MB_TC_KAPALibraryQuantv4_8].json @@ -34667,6 +34667,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json index bd05f58334f..0dd0410636f 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e42e36e3ca][OT2_X_v2_13_None_None_PythonSyntaxError].json @@ -45,6 +45,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "apiLevel": "2.13", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json index 44fbc26f5b6..1e4573b1d8e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4660ca6df][OT2_S_v4_P300S_None_MM_TM_TM_MOAMTemps].json @@ -2580,6 +2580,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "AA BB", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json index 013da0c0d7d..f6c1ad84067 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e496fec176][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_default].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json index 7f0ba6fd654..aca7454ff36 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e4b5b30b2e][Flex_S_v2_18_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json index a0e23ed018b..803f4133451 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e71b031f47][pl_Illumina_DNA_PCR_Free].json @@ -27030,6 +27030,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "DNA sample of known quantity", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json index 3ab5889bbf7..ea9fbf3efb7 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e84e23a4ea][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_top_edge].json @@ -1249,6 +1249,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json index ca6f70d1692..81ebf160345 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e8f451df45][Flex_S_v2_20_96_None_Column3_SINGLE_].json @@ -26843,6 +26843,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json index 368bbe05d9b..709b448717c 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed1e64c539][Flex_X_v2_16_NO_PIPETTES_TM_ModuleInCol2].json @@ -103,6 +103,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json index 61a7e9595ff..ed5d5a67171 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed26635ff7][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_consolidate_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json index 00f911388c0..d946aae6d9d 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ed2f3800b6][Flex_S_2_18_P1000M_P50M_GRIP_HS_TM_MB_TC_IlluminaDNAPrep24xV4_7].json @@ -40778,6 +40778,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json index 4bcefec1199..d1a7a88d075 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f0efddcd7d][Flex_X_v2_16_P1000_96_DropTipsWithNoTrash].json @@ -1448,6 +1448,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json index d1feceae4d0..d454695d871 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f24bb0b4d9][Flex_S_v2_15_P1000_96_GRIP_HS_MB_TC_TM_IlluminaDNAPrep96PART3].json @@ -18009,6 +18009,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json index 4e89581c149..3d7f6d10b51 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f301704f56][OT2_S_v6_P300M_P300S_HS_HS_NormalUseWithTransfer].json @@ -7101,6 +7101,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json index ab9fd95e4c0..d86eae54045 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f345e8e33a][OT2_S_v4_P300M_P20S_MM_TM_TC1_PD40].json @@ -10615,6 +10615,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "NN MM", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json index b12618b009e..e0c21a82e55 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f37bb0ec36][OT2_S_v2_16_NO_PIPETTES_verifyDoesNotDeadlock].json @@ -29,6 +29,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [], diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json index f8f121ce092..50ab65351e1 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f51172f73b][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_Smoke].json @@ -15386,6 +15386,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json index d452cf7ab52..950c5ee4395 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f5f3b9c5bb][Flex_X_v2_16_P1000_96_TM_ModuleAndWasteChuteConflict].json @@ -1339,6 +1339,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Derek Maggio ", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json index 2c598934321..e4fed39c549 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f639acc89d][Flex_S_v2_15_NO_PIPETTES_TC_verifyThermocyclerLoadedSlots].json @@ -180,6 +180,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json index 8ca9a88cdbf..920a648041a 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f6c1ddbb32][pl_ExpressPlex_Pooling_Final].json @@ -36949,6 +36949,7 @@ } } ], + "liquidClasses": [], "liquids": [ { "description": "Amplified Libraries_1", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json index 04d54b06b4e..7ad30e9d04e 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f7085d7134][Flex_X_v2_16_P1000_96_TC_pipetteCollisionWithThermocyclerLidClips].json @@ -1385,6 +1385,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": {}, "modules": [ diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json index 3152a671909..acf7455e286 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f834b97da1][Flex_S_v2_16_P1000_96_GRIP_HS_MB_TC_TM_DeckConfiguration1].json @@ -14192,6 +14192,7 @@ "location": "offDeck" } ], + "liquidClasses": [], "liquids": [ { "description": "High Quality H₂O", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json index 09e15f48097..1fdb58d69ab 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f86713b4d4][Flex_X_v2_20_96_None_Overrides_TooTallLabware_Override_distribute_source_collision].json @@ -3878,6 +3878,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "description": "oooo", diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json index 1652972327b..fe2e22fae05 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f88b7d6e30][Flex_X_v2_18_NO_PIPETTES_Overrides_BadTypesInRTP_Override_wrong_type_in_display_name].json @@ -42,6 +42,7 @@ } ], "labware": [], + "liquidClasses": [], "liquids": [], "metadata": { "protocolName": "Description Too Long 2.18" diff --git a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json index 13f15c638d0..cd25845d931 100644 --- a/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json +++ b/analyses-snapshot-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[fc60ef9cbd][OT2_S_v2_16_P300M_P20S_HS_TC_TM_dispense_changes].json @@ -3105,6 +3105,7 @@ } } ], + "liquidClasses": [], "liquids": [], "metadata": { "author": "Opentrons Engineering ", diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index a6279d12145..bf8596b66d7 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -120,6 +120,7 @@ export interface Runs { export interface RunCurrentStateData { estopEngaged: boolean activeNozzleLayouts: Record // keyed by pipetteId + tipStates: Record // keyed by pipetteId placeLabwareState?: PlaceLabwareState } @@ -218,3 +219,7 @@ export interface PlaceLabwareState { location: OnDeckLabwareLocation shouldPlaceDown: boolean } + +export interface TipStates { + hasTip: boolean +} diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 5ab0fdaad76..c8d1e8e7f97 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -99,7 +99,7 @@ # use rst_prolog to hold the subsitution # update the apiLevel value whenever a new minor version is released rst_prolog = f""" -.. |apiLevel| replace:: 2.20 +.. |apiLevel| replace:: 2.21 .. |release| replace:: {release} """ @@ -445,7 +445,6 @@ ("py:class", r".*protocol_api\.config.*"), ("py:class", r".*opentrons_shared_data.*"), ("py:class", r".*protocol_api._parameters.Parameters.*"), - ("py:class", r".*AbsorbanceReaderContext"), ("py:class", r".*RobotContext"), # shh it's a secret (for now) ("py:class", r'.*AbstractLabware|APIVersion|LabwareLike|LoadedCoreMap|ModuleTypes|NoneType|OffDeckType|ProtocolCore|WellCore'), # laundry list of not fully qualified things ] diff --git a/api/docs/v2/modules/absorbance_plate_reader.rst b/api/docs/v2/modules/absorbance_plate_reader.rst new file mode 100644 index 00000000000..9f96d5e90d3 --- /dev/null +++ b/api/docs/v2/modules/absorbance_plate_reader.rst @@ -0,0 +1,147 @@ +:og:description: How to use the Absorbance Plate Reader Module in a Python protocol. + +.. _absorbance-plate-reader-module: + +****************************** +Absorbance Plate Reader Module +****************************** + +The Absorbance Plate Reader Module is an on-deck microplate spectrophotometer that works with the Flex robot only. The module uses light absorbance to determine sample concentrations in 96-well plates. + +The Absorbance Plate Reader is represented in code by an :py:class:`.AbsorbanceReaderContext` object, which has methods for moving the module lid with the Flex Gripper, initializing the module to read at a single wavelength or multiple wavelengths, and reading a plate. With the Python Protocol API, you can process plate reader data immediately in your protocol or export it to a CSV for post-run use. + +This page explains the actions necessary for using the Absorbance Plate Reader. These combine to form the typical reader workflow: + + 1. Close the lid with no plate inside + 2. Initialize the reader + 3. Open the lid + 4. Move a plate onto the module + 5. Close the lid + 6. Read the plate + + +Loading and Deck Slots +====================== + +The Absorbance Plate Reader can only be loaded in slots A3–D3. If you try to load it in any other slot, the API will raise an error. The module's caddy is designed such that the detection unit is in deck column 3 and the special staging area for the lid/illumination unit is in deck column 4. You can't load or move other labware on the Absorbance Plate Reader caddy in deck column 4, even while the lid is in the closed position (on top of the detection unit in deck column 3). + +The examples in this section will use an Absorbance Plate Reader Module loaded as follows:: + + pr_mod = protocol.load_module( + module_name="absorbanceReaderV1", + location="D3" + ) + +.. versionadded:: 2.21 + +Lid Control +=========== + +Flex uses the gripper to move the lid between its two positions. + + - :py:meth:`~.AbsorbanceReaderContext.open_lid()` moves the lid to the righthand side of the caddy, in deck column 4. + - :py:meth:`~.AbsorbanceReaderContext.close_lid()` moves the lid onto the detection unit, in deck column 3. + +If you call ``open_lid()`` or ``close_lid()`` and the lid is already in the corresponding position, the method will succeed immediately. You can also check the position of the lid with :py:meth:`~.AbsorbanceReaderContext.is_lid_on()`. + +You need to call ``close_lid()`` before initializing the reader, even if the reader was in the closed position at the start of the protocol. + +.. warning:: + Do not move the lid manually, during or outside of a protocol. The API does not allow manual lid movement because there is a risk of damaging the module. + +.. _absorbance-initialization: + +Initialization +============== + +Initializing the reader prepares it to read a plate later in your protocol. The :py:meth:`.AbsorbanceReaderContext.initialize` method accepts parameters for the number of readings you want to take, the wavelengths to read, and whether you want to compare the reading to a reference wavelength. In the default hardware configuration, the supported wavelengths are 450 nm (blue), 562 nm (green), 600 nm (orange), and 650 nm (red). + +The module uses these parameters immediately to perform the physical initialization. Additionally, the API preserves these values and uses them when you read the plate later in your protocol. + +Let's take a look at examples of how to combine these parameters to prepare different types of readings. The simplest reading measures one wavelength, with no reference wavelength:: + + pr_mod.initialize(mode="single", wavelengths=[450]) + +.. versionadded:: 2.21 + +Now the reader is prepared to read at 450 nm. Note that the ``wavelengths`` parameter always takes a list of integer wavelengths, even when only reading a single wavelength. + +This example can be extended by adding a reference wavelength:: + + pr_mod.initialize( + mode="single", wavelengths=[450], reference_wavelength=[562] + ) + +When configured this way, the module will read twice. In the :ref:`output data `, the values read for ``reference_wavelength`` will be subtracted from the values read for the single member of ``wavelengths``. This is useful for normalization, or to correct for background interference in wavelength measurements. + +The reader can also be initialized to take multiple measurements. When ``mode="multi"``, the ``wavelengths`` list can have up to six elements. This example will initialize the reader to read at three wavelengths:: + + pr_mod.initialize(mode="multi", wavelengths=[450, 562, 600]) + +You can't use a reference wavelength when performing multiple measurements. + + +Reading a Plate +=============== + +Use :py:meth:`.AbsorbanceReaderContext.read` to have the module read the plate, using the parameters that you specified during initialization:: + + pr_data = pr_mod.read() + +.. versionadded:: 2.21 + +The ``read()`` method returns the results in a dictionary, which the above example saves to the variable ``pr_data``. + +If you need to access this data after the conclusion of your protocol, add the ``export_filename`` parameter to instruct the API to output a CSV file, which is available in the Opentrons App by going to your Flex and viewing Recent Protocol Runs:: + + pr_data = pr_mod.read(export_filename="plate_data") + +In the above example, the API both saves the data to a variable and outputs a CSV file. If you only need the data post-run, you can omit the variable assignment. + +.. _plate-reader-data: + +Using Plate Reader Data +======================= + +There are two ways to use output data from the Absorbance Plate Reader: + +- Within your protocol as a nested dictionary object. +- Outside of your protocol, as a tabular CSV file. + +The two formats are structured differently, even though they contain the same measurement data. + +Dictionary Data +--------------- + +The dictionary object returned by ``read()`` has two nested levels. The keys at the top level are the wavelengths you provided to ``initialize()``. The keys at the second level are string names of each of the 96 wells, ``"A1"`` through ``"H12"``. The values at the second level are the measured values for each wells. These values are floating point numbers, representing the optical density (OD) of the samples in each well. OD ranges from 0.0 (low sample concentration) to 4.0 (high sample concentration). + +The nested dictionary structure allows you to access results by index later in your protocol. This example initializes a multiple read and then accesses different portions of the results:: + + # initializing and reading + pr_mod.initialize(mode="multi", wavelengths=[450, 600]) + pr_mod.open_lid() + protocol.move_labware(plate, pr_mod, use_gripper=True) + pr_mod.close_lid() + pr_data = pr_mod.read() + + # accessing results + pr_data[450]["A1"] # value for well A1 at 450 nm + pr_data[600]["H12"] # value for well H12 at 600 nm + pr_data[450] # dict of all wells at 450 nm + +You can write additional code to transform this data in any way that you need. For example, you could use a list comprehension to create a list of only the 450 nm values for column 1, ordered by well from A1 to H1:: + + [pr_data[450][w.well_name] for w in plate.columns()[0]] + +.. _absorbance-csv: + +CSV data +-------- + +The CSV exported when specifying ``export_filename`` consists of tabular data followed by additional information. Each measurement produces 9 rows in the CSV file, representing the layout of the well plate that has been read. These rows form a table with numeric labels in the first row and alphabetic labels in the first column, as you would see on physical labware. Each "cell" of the table contains the measured OD value for the well (0.0–4.0) in the corresponding position on the plate. + +Additional information, starting with one blank labware grid, is output at the end of the file. The last few lines of the file list the sample wavelengths, serial number of the module, and timestamps for when measurement started and finished. + +Each output file for your protocol is available in the Opentrons App by going to your Flex and viewing Recent Protocol Runs. After downloading the file from your Flex, you can read it with any software that reads CSV files, and you can write additional code to parse and act upon its contents. + +You can also select the output CSV as the value of a CSV runtime parameter in a subsequent protocol. When you :ref:`parse the CSV data `, make sure to set ``detect_dialect=False``, or the API will raise an error. \ No newline at end of file diff --git a/api/docs/v2/modules/setup.rst b/api/docs/v2/modules/setup.rst index c6badd82954..a0cbe18bf0e 100644 --- a/api/docs/v2/modules/setup.rst +++ b/api/docs/v2/modules/setup.rst @@ -66,7 +66,7 @@ Available Modules The first parameter of :py:meth:`.ProtocolContext.load_module` is the module's *API load name*. The load name tells your robot which module you're going to use in a protocol. The table below lists the API load names for the currently available modules. .. table:: - :widths: 4 5 2 + :widths: 4 4 2 +--------------------+-------------------------------+---------------------------+ | Module | API Load Name | Introduced in API Version | @@ -95,6 +95,9 @@ The first parameter of :py:meth:`.ProtocolContext.load_module` is the module's | Magnetic Block | ``magneticBlockV1`` | 2.15 | | GEN1 | | | +--------------------+-------------------------------+---------------------------+ + | Absorbance Plate | ``absorbanceReaderV1`` | 2.21 | + | Reader Module | | | + +--------------------+-------------------------------+---------------------------+ Some modules were added to our Python API later than others, and others span multiple hardware generations. When writing a protocol that requires a module, make sure your ``requirements`` or ``metadata`` code block specifies an :ref:`API version ` high enough to support all the module generations you want to use. @@ -124,7 +127,7 @@ Any :ref:`custom labware ` added to your Opentrons App is als Module and Labware Compatibility -------------------------------- -It's your responsibility to ensure the labware and module combinations you load together work together. The Protocol API won't raise a warning or error if you load an unusual combination, like placing a tube rack on a Thermocycler. See `What labware can I use with my modules? `_ for more information about labware/module combinations. +It's your responsibility to ensure the labware and module combinations you load together work together. The API generally won't raise a warning or error if you load an unusual combination, like placing a tube rack on a Thermocycler. The API will raise an error if you try to load a labware on an unsupported adapter. When working with custom labware and module adapters, be sure to add stacking offsets for the adapter to your custom labware definition. Additional Labware Parameters diff --git a/api/docs/v2/modules/thermocycler.rst b/api/docs/v2/modules/thermocycler.rst index 9322e0a96f0..17d57e84292 100644 --- a/api/docs/v2/modules/thermocycler.rst +++ b/api/docs/v2/modules/thermocycler.rst @@ -15,7 +15,7 @@ The examples in this section will use a Thermocycler Module GEN2 loaded as follo .. code-block:: python tc_mod = protocol.load_module(module_name="thermocyclerModuleV2") - plate = tc_mod.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt") + plate = tc_mod.load_labware(name="opentrons_96_wellplate_200ul_pcr_full_skirt") .. versionadded:: 2.13 @@ -139,6 +139,70 @@ However, this code would generate 60 lines in the protocol's run log, while exec .. versionadded:: 2.0 +Auto-sealing Lids +================= + +Starting in robot software version 8.2.0, you can use the Opentrons Tough PCR Auto-sealing Lid to reduce evaporation on the Thermocycler. The auto-sealing lids are designed for automated use with the Flex Gripper, although you can move them manually if needed. They also work with the Opentrons Flex Deck Riser adapter, which keeps lids away from the unsterilized deck and provides better access for the gripper. + +Use the following API load names for the auto-sealing lid and deck riser: + +.. list-table:: + :header-rows: 1 + + * - Labware + - API load name + * - Opentrons Tough PCR Auto-sealing Lid + - ``opentrons_tough_pcr_auto_sealing_lid`` + * - Opentrons Flex Deck Riser + - ``opentrons_flex_deck_riser`` + +Load the riser directly onto the deck with :py:meth:`.ProtocolContext.load_adapter`. Load the auto-sealing lid onto a compatible location (the deck, the riser, or another lid) with the appropriate ``load_labware()`` method. You can create a stack of up to five auto-sealing lids. If you try to stack more than five lids, the API will raise an error. + +Setting up the riser and preparing a lid to use on the Thermocycler generally consists of the following steps: + + 1. Load the riser on the deck. + 2. Load the lids onto the adapter. + 3. Load or move a PCR plate onto the Thermocycler. + 4. Move a lid onto the PCR plate. + 5. Close the Thermocycler. + +The following code sample shows how to perform these steps, using the riser and three auto-sealing lids. In a full protocol, you would likely have additional steps, such as pipetting to or from the PCR plate. + +.. code-block:: python + + # load riser + riser = protocol.load_adapter( + load_name="opentrons_flex_deck_riser", location="A2" + ) + + # load three lids + lid_1 = riser.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_2 = lid_1.load_labware("opentrons_tough_pcr_auto_sealing_lid") + lid_3 = lid_2.load_labware("opentrons_tough_pcr_auto_sealing_lid") + + # load plate on Thermocycler + plate = protocol.load_labware( + load_name="opentrons_96_wellplate_200ul_pcr_full_skirt", location=tc_mod + ) + + # move lid to PCR plate + protocol.move_labware(labware=lid_3, new_location=plate, use_gripper=True) + + # close Thermocycler + tc_mod.close_lid() + +.. warning:: + When using the auto-sealing lids, `do not` affix a rubber automation seal to the inside of the Thermocycler lid. The Thermocycler will not close properly. + +When you're finished with a lid, use the gripper to dispose of it in either the waste chute or a trash bin:: + + tc_mod.open_lid() + protocol.move_labware(labware=lid_3, new_location=trash, use_gripper=True) + +.. versionadded:: 2.16 + :py:class:`.TrashBin` and :py:class:`.WasteChute` objects can accept lids. + +You can then move the PCR plate off of the Thermocycler. The Flex Gripper can't move a plate that has a lid on top of it. Always move the lid first, then the plate. Changes with the GEN2 Thermocycler Module ========================================= diff --git a/api/docs/v2/new_modules.rst b/api/docs/v2/new_modules.rst index 956a2bc7989..594ceca3867 100644 --- a/api/docs/v2/new_modules.rst +++ b/api/docs/v2/new_modules.rst @@ -8,6 +8,7 @@ Hardware Modules .. toctree:: modules/setup + modules/absorbance_plate_reader modules/heater_shaker modules/magnetic_block modules/magnetic_module @@ -17,13 +18,14 @@ Hardware Modules Hardware modules are powered and unpowered deck-mounted peripherals. The Flex and OT-2 are aware of deck-mounted powered modules when they're attached via a USB connection and used in an uploaded protocol. The robots do not know about unpowered modules until you use one in a protocol and upload it to the Opentrons App. -Powered modules include the Heater-Shaker Module, Magnetic Module, Temperature Module, and Thermocycler Module. The 96-well Magnetic Block is an unpowered module. +Powered modules include the Absorbance Plate Reader Module, Heater-Shaker Module, Magnetic Module, Temperature Module, and Thermocycler Module. The 96-well Magnetic Block is an unpowered module. Pages in this section of the documentation cover: - :ref:`Setting up modules and their labware `. - Working with the module contexts for each type of module. + - :ref:`Absorbance Plate Reader Module ` - :ref:`Heater-Shaker Module ` - :ref:`Magnetic Block ` - :ref:`Magnetic Module ` diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index a71ad5cf4a2..2ce4c39e3cc 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -53,29 +53,53 @@ Wells and Liquids Modules ======= +Absorbance Plate Reader +----------------------- + +.. autoclass:: opentrons.protocol_api.AbsorbanceReaderContext + :members: + :exclude-members: broker, geometry, load_labware_object, load_adapter, load_adapter_from_definition + :inherited-members: + + +Heater-Shaker +------------- + .. autoclass:: opentrons.protocol_api.HeaterShakerContext :members: :exclude-members: broker, geometry, load_labware_object :inherited-members: +Magnetic Block +-------------- + .. autoclass:: opentrons.protocol_api.MagneticBlockContext :members: :exclude-members: broker, geometry, load_labware_object :inherited-members: +Magnetic Module +--------------- + .. autoclass:: opentrons.protocol_api.MagneticModuleContext :members: :exclude-members: calibrate, broker, geometry, load_labware_object :inherited-members: +Temperature Module +------------------ + .. autoclass:: opentrons.protocol_api.TemperatureModuleContext :members: :exclude-members: start_set_temperature, await_temperature, broker, geometry, load_labware_object :inherited-members: +Thermocycler +------------ + .. autoclass:: opentrons.protocol_api.ThermocyclerContext :members: - :exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry, load_labware_object + :exclude-members: total_step_count, current_cycle_index, total_cycle_count, hold_time, ramp_rate, current_step_index, broker, geometry, load_labware_object, load_adapter, load_adapter_from_definition :inherited-members: diff --git a/api/docs/v2/versioning.rst b/api/docs/v2/versioning.rst index 72430f9104f..935011f61dd 100644 --- a/api/docs/v2/versioning.rst +++ b/api/docs/v2/versioning.rst @@ -68,7 +68,7 @@ The maximum supported API version for your robot is listed in the Opentrons App If you upload a protocol that specifies a higher API level than the maximum supported, your robot won't be able to analyze or run your protocol. You can increase the maximum supported version by updating your robot software and Opentrons App. -Opentrons robots running the latest software (8.0.0) support the following version ranges: +Opentrons robots running the latest software (8.2.0) support the following version ranges: * **Flex:** version 2.15–|apiLevel|. * **OT-2:** versions 2.0–|apiLevel|. @@ -84,6 +84,8 @@ This table lists the correspondence between Protocol API versions and robot soft +-------------+------------------------------+ | API Version | Introduced in Robot Software | +=============+==============================+ +| 2.21 | 8.2.0 | ++-------------+------------------------------+ | 2.20 | 8.0.0 | +-------------+------------------------------+ | 2.19 | 7.3.1 | @@ -134,6 +136,12 @@ This table lists the correspondence between Protocol API versions and robot soft Changes in API Versions ======================= +Version 2.21 +------------ +- Adds :py:class:`.AbsorbanceReaderContext` to support the :ref:`Absorbance Plate Reader Module `. Use the load name ``absorbanceReaderV1`` with :py:meth:`.ProtocolContext.load_module` to add an Absorbance Plate Reader to a protocol. +- :ref:`Liquid presence detection ` now only checks on the first aspiration of the :py:meth:`.mix` cycle. +- Improved the run log output of :py:meth:`.ThermocyclerContext.execute_profile`. + Version 2.20 ------------ diff --git a/api/pytest.ini b/api/pytest.ini index a8e3bbb1933..61288b3f3c1 100644 --- a/api/pytest.ini +++ b/api/pytest.ini @@ -5,3 +5,9 @@ markers = ot3_only: Test only functions using the OT3 hardware addopts = --color=yes --strict-markers asyncio_mode = auto + +# TODO this should be looked into being removed upon updating the Decoy library. The purpose of this warning is to +# catch missing attributes, but it raises for any property referenced in a test which accounts for about ~250 warnings +# which aren't serving any useful purpose and obscure other warnings. +filterwarnings = + ignore::decoy.warnings.MissingSpecAttributeWarning diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 1253f7e92fd..761f1f604f3 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,10 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. + ## Internal Release 2.2.0-alpha.1 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 8489da83d68..4c994fcf630 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -53,6 +53,7 @@ LoadedPipette, LoadedModule, Liquid, + LiquidClassRecordWithId, StateSummary, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -333,6 +334,7 @@ async def _do_analyze( wells=[], hasEverEnteredErrorRecovery=False, files=[], + liquidClasses=[], ), parameters=[], ) @@ -399,6 +401,7 @@ async def _analyze( pipettes=analysis.state_summary.pipettes, modules=analysis.state_summary.modules, liquids=analysis.state_summary.liquids, + liquidClasses=analysis.state_summary.liquidClasses, ) _call_for_output_of_kind( @@ -486,4 +489,5 @@ class AnalyzeResults(BaseModel): pipettes: List[LoadedPipette] modules: List[LoadedModule] liquids: List[Liquid] + liquidClasses: List[LiquidClassRecordWithId] errors: List[ErrorOccurrence] diff --git a/api/src/opentrons/config/defaults_ot3.py b/api/src/opentrons/config/defaults_ot3.py index 55565745d3a..53fab18392c 100644 --- a/api/src/opentrons/config/defaults_ot3.py +++ b/api/src/opentrons/config/defaults_ot3.py @@ -75,6 +75,7 @@ DEFAULT_GRIPPER_MOUNT_OFFSET: Final[Offset] = (84.55, -12.75, 93.85) DEFAULT_SAFE_HOME_DISTANCE: Final = 5 DEFAULT_CALIBRATION_AXIS_MAX_SPEED: Final = 30 +DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED: Final = 90 DEFAULT_MAX_SPEEDS: Final[ByGantryLoad[Dict[OT3AxisKind, float]]] = ByGantryLoad( high_throughput={ diff --git a/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py b/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py index 0460a016229..6f405c9af32 100644 --- a/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py +++ b/api/src/opentrons/drivers/absorbance_reader/async_byonoy.py @@ -23,7 +23,8 @@ SN_PARSER = re.compile(r'ATTRS{serial}=="(?P.+?)"') -VERSION_PARSER = re.compile(r"Absorbance (?PV\d+\.\d+\.\d+)") +# match semver V0.0.0 (old format) or one integer (latest format) +VERSION_PARSER = re.compile(r"(?P(V\d+\.\d+\.\d+|^\d+$))") SERIAL_PARSER = re.compile(r"(?P(OPT|BYO)[A-Z]{3}[0-9]+)") @@ -156,10 +157,10 @@ async def get_device_information(self) -> Dict[str, str]: func=partial(self._interface.get_device_information, handle), ) self._raise_if_error(err.name, f"Error getting device information: {err}") - serial_match = SERIAL_PARSER.fullmatch(device_info.sn) - version_match = VERSION_PARSER.match(device_info.version) + serial_match = SERIAL_PARSER.match(device_info.sn) + version_match = VERSION_PARSER.search(device_info.version) serial = serial_match["serial"].strip() if serial_match else "OPTMAA00000" - version = version_match["version"].lower() if version_match else "v0.0.0" + version = version_match["version"].lower() if version_match else "v0" info = { "serial": serial, "version": version, diff --git a/api/src/opentrons/hardware_control/backends/flex_protocol.py b/api/src/opentrons/hardware_control/backends/flex_protocol.py index c5294938fa0..e5bc7ba1905 100644 --- a/api/src/opentrons/hardware_control/backends/flex_protocol.py +++ b/api/src/opentrons/hardware_control/backends/flex_protocol.py @@ -60,6 +60,14 @@ def restore_system_constraints(self) -> AsyncIterator[None]: def grab_pressure(self, channels: int, mount: OT3Mount) -> AsyncIterator[None]: ... + def set_pressure_sensor_available( + self, pipette_axis: Axis, available: bool + ) -> None: + ... + + def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool: + ... + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: ... @@ -69,6 +77,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: ... + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + ... + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 66ffc1efab1..a7c30677910 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -50,6 +50,7 @@ get_system_constraints, get_system_constraints_for_calibration, get_system_constraints_for_plunger_acceleration, + get_system_constraints_for_emulsifying_pipette, ) from .tip_presence_manager import TipPresenceManager @@ -197,6 +198,7 @@ PipetteLiquidNotFoundError, CommunicationError, PythonException, + UnsupportedHardwareCommand, ) from .subsystem_manager import SubsystemManager @@ -362,6 +364,7 @@ def __init__( self._configuration.motion_settings, GantryLoad.LOW_THROUGHPUT ) ) + self._pressure_sensor_available: Dict[NodeId, bool] = {} @asynccontextmanager async def restore_system_constraints(self) -> AsyncIterator[None]: @@ -380,6 +383,16 @@ async def grab_pressure( async with grab_pressure(channels, tool, self._messenger): yield + def set_pressure_sensor_available( + self, pipette_axis: Axis, available: bool + ) -> None: + pip_node = axis_to_node(pipette_axis) + self._pressure_sensor_available[pip_node] = available + + def get_pressure_sensor_available(self, pipette_axis: Axis) -> bool: + pip_node = axis_to_node(pipette_axis) + return self._pressure_sensor_available[pip_node] + def update_constraints_for_calibration_with_gantry_load( self, gantry_load: GantryLoad, @@ -393,6 +406,18 @@ def update_constraints_for_calibration_with_gantry_load( f"Set system constraints for calibration: {self._move_manager.get_constraints()}" ) + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + self._move_manager.update_constraints( + get_system_constraints_for_emulsifying_pipette( + self._configuration.motion_settings, gantry_load, mount + ) + ) + log.debug( + f"Set system constraints for emulsifying pipette: {self._move_manager.get_constraints()}" + ) + def update_constraints_for_gantry_load(self, gantry_load: GantryLoad) -> None: self._move_manager.update_constraints( get_system_constraints(self._configuration.motion_settings, gantry_load) @@ -762,7 +787,8 @@ async def _runner_coroutine( for runner, is_gear_move in maybe_runners if runner ] - async with self._monitor_overpressure(pipettes_moving): + checked_moving_pipettes = self._pipettes_to_monitor_pressure(pipettes_moving) + async with self._monitor_overpressure(checked_moving_pipettes): all_positions = await asyncio.gather(*coros) for positions, handle_gear_move in all_positions: @@ -871,7 +897,8 @@ async def home( moving_pipettes = [ axis_to_node(ax) for ax in checked_axes if ax in Axis.pipette_axes() ] - async with self._monitor_overpressure(moving_pipettes): + checked_moving_pipettes = self._pipettes_to_monitor_pressure(moving_pipettes) + async with self._monitor_overpressure(checked_moving_pipettes): positions = await asyncio.gather(*coros) # TODO(CM): default gear motor homing routine to have some acceleration if Axis.Q in checked_axes: @@ -886,6 +913,9 @@ async def home( self._handle_motor_status_response(position) return axis_convert(self._position, 0.0) + def _pipettes_to_monitor_pressure(self, pipettes: List[NodeId]) -> List[NodeId]: + return [pip for pip in pipettes if self._pressure_sensor_available[pip]] + def _filter_move_group(self, move_group: MoveGroup) -> MoveGroup: new_group: MoveGroup = [] for step in move_group: @@ -1008,6 +1038,7 @@ def _lookup_serial_key(pipette_name: FirmwarePipetteName) -> str: lookup_name = { FirmwarePipetteName.p1000_single: "P1KS", FirmwarePipetteName.p1000_multi: "P1KM", + FirmwarePipetteName.p1000_multi_em: "P1KP", FirmwarePipetteName.p50_single: "P50S", FirmwarePipetteName.p50_multi: "P50M", FirmwarePipetteName.p1000_96: "P1KH", @@ -1472,6 +1503,11 @@ async def liquid_probe( ) -> float: head_node = axis_to_node(Axis.by_mount(mount)) tool = sensor_node_for_pipette(OT3Mount(mount.value)) + if tool not in self._pipettes_to_monitor_pressure([tool]): + raise UnsupportedHardwareCommand( + "Liquid Presence Detection not available on this pipette." + ) + positions = await liquid_probe( messenger=self._messenger, tool=tool, diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index e487f963ece..533fffe5642 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -234,6 +234,11 @@ def update_constraints_for_calibration_with_gantry_load( ) -> None: self._sim_gantry_load = gantry_load + def update_constraints_for_emulsifying_pipette( + self, mount: OT3Mount, gantry_load: GantryLoad + ) -> None: + pass + def update_constraints_for_plunger_acceleration( self, mount: OT3Mount, acceleration: float, gantry_load: GantryLoad ) -> None: diff --git a/api/src/opentrons/hardware_control/backends/ot3utils.py b/api/src/opentrons/hardware_control/backends/ot3utils.py index e3952cbd907..57e74537bfd 100644 --- a/api/src/opentrons/hardware_control/backends/ot3utils.py +++ b/api/src/opentrons/hardware_control/backends/ot3utils.py @@ -2,7 +2,10 @@ from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional from typing_extensions import Literal from logging import getLogger -from opentrons.config.defaults_ot3 import DEFAULT_CALIBRATION_AXIS_MAX_SPEED +from opentrons.config.defaults_ot3 import ( + DEFAULT_CALIBRATION_AXIS_MAX_SPEED, + DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED, +) from opentrons.config.types import OT3MotionSettings, OT3CurrentSettings, GantryLoad from opentrons.hardware_control.types import ( Axis, @@ -300,6 +303,31 @@ def get_system_constraints_for_plunger_acceleration( return new_constraints +def get_system_constraints_for_emulsifying_pipette( + config: OT3MotionSettings, + gantry_load: GantryLoad, + mount: OT3Mount, +) -> "SystemConstraints[Axis]": + old_constraints = config.by_gantry_load(gantry_load) + new_constraints = {} + axis_kinds = set([k for _, v in old_constraints.items() for k in v.keys()]) + for axis_kind in axis_kinds: + for axis in Axis.of_kind(axis_kind): + if axis == Axis.of_main_tool_actuator(mount): + _max_speed = float(DEFAULT_EMULSIFYING_PIPETTE_AXIS_MAX_SPEED) + else: + _max_speed = old_constraints["default_max_speed"][axis_kind] + new_constraints[axis] = AxisConstraints.build( + max_acceleration=old_constraints["acceleration"][axis_kind], + max_speed_discont=old_constraints["max_speed_discontinuity"][axis_kind], + max_direction_change_speed_discont=old_constraints[ + "direction_change_speed_discontinuity" + ][axis_kind], + max_speed=_max_speed, + ) + return new_constraints + + def _convert_to_node_id_dict( axis_pos: Coordinates[Axis, CoordinateValue], ) -> NodeIdMotionValues: @@ -642,6 +670,7 @@ def update( FirmwareGripperjawState.force_controlling_home: GripperJawState.HOMED_READY, FirmwareGripperjawState.force_controlling: GripperJawState.GRIPPING, FirmwareGripperjawState.position_controlling: GripperJawState.HOLDING, + FirmwareGripperjawState.stopped: GripperJawState.STOPPED, } diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index a6773cb9184..981e95e114e 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -20,6 +20,7 @@ PipetteConfigurations, SupportedTipsDefinition, PipetteBoundingBoxOffsetDefinition, + AvailableSensorDefinition, ) from opentrons_shared_data.gripper import ( GripperModel, @@ -100,6 +101,9 @@ class PipetteDict(InstrumentDict): pipette_bounding_box_offsets: PipetteBoundingBoxOffsetDefinition current_nozzle_map: NozzleMap lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: AvailableSensorDefinition class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 7fc15c4c2d3..2d63342cf19 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -28,7 +28,7 @@ CommandPreconditionViolated, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -584,21 +584,9 @@ def get_nominal_tip_overlap_dictionary_by_configuration( # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, action, self._active_tip_settings, self._pipetting_function_version + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index 931c99fd4c6..7bd41e02e74 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -260,6 +260,13 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 109747ea1b9..b9355874906 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -27,7 +27,7 @@ InvalidInstrumentData, ) from opentrons_shared_data.pipette.ul_per_mm import ( - piecewise_volume_conversion, + calculate_ul_per_mm, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) @@ -41,6 +41,7 @@ UlPerMmAction, PipetteName, PipetteModel, + Quirks, ) from opentrons_shared_data.pipette import ( load_data as load_pipette_data, @@ -225,6 +226,9 @@ def active_tip_settings(self) -> SupportedTipsDefinition: def push_out_volume(self) -> float: return self._active_tip_settings.default_push_out_volume + def is_high_speed_pipette(self) -> bool: + return Quirks.highSpeed in self._config.quirks + def act_as(self, name: PipetteName) -> None: """Reconfigure to act as ``name``. ``name`` must be either the actual name of the pipette, or a name in its back-compatibility @@ -529,23 +533,13 @@ def tip_presence_responses(self) -> int: # want this to unbounded. @functools.lru_cache(maxsize=100) def ul_per_mm(self, ul: float, action: UlPerMmAction) -> float: - if action == "aspirate": - fallback = self._active_tip_settings.aspirate.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.aspirate.default.get( - self._pipetting_function_version, fallback - ) - elif action == "blowout": - return self._config.shaft_ul_per_mm - else: - fallback = self._active_tip_settings.dispense.default[ - PIPETTING_FUNCTION_FALLBACK_VERSION - ] - sequence = self._active_tip_settings.dispense.default.get( - self._pipetting_function_version, fallback - ) - return piecewise_volume_conversion(ul, sequence) + return calculate_ul_per_mm( + ul, + action, + self._active_tip_settings, + self._pipetting_function_version, + self._config.shaft_ul_per_mm, + ) def __str__(self) -> str: return "{} current volume {}ul critical point: {} at {}".format( @@ -585,6 +579,7 @@ def as_dict(self) -> "Pipette.DictType": "versioned_tip_overlap": self.tip_overlap, "back_compat_names": self._config.pipette_backcompat_names, "supported_tips": self.liquid_class.supported_tips, + "shaft_ul_per_mm": self._config.shaft_ul_per_mm, } ) return self._config_as_dict diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index f64078fcbff..dda5031a8a3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -282,6 +282,13 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: "pipette_bounding_box_offsets" ] = instr.config.pipette_bounding_box_offsets result["lld_settings"] = instr.config.lld_settings + result["plunger_positions"] = { + "top": instr.plunger_positions.top, + "bottom": instr.plunger_positions.bottom, + "blow_out": instr.plunger_positions.blow_out, + "drop_tip": instr.plunger_positions.drop_tip, + } + result["shaft_ul_per_mm"] = instr.config.shaft_ul_per_mm return cast(PipetteDict, result) @property diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index bd828cd525f..af170484150 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -32,6 +32,7 @@ ) from opentrons_shared_data.pipette import ( pipette_load_name_conversions as pipette_load_name, + pipette_definition, ) from opentrons_shared_data.robot.types import RobotType @@ -634,10 +635,43 @@ async def cache_pipette( self._feature_flags.use_old_aspiration_functions, ) self._pipette_handler.hardware_instruments[mount] = p + + if self._pipette_handler.has_pipette(mount): + self._confirm_pipette_motion_constraints(mount) + + if config is not None: + self._set_pressure_sensor_available(mount, instrument_config=config) + # TODO (lc 12-5-2022) Properly support backwards compatibility # when applicable return skipped + def _confirm_pipette_motion_constraints( + self, + mount: OT3Mount, + ) -> None: + if self._pipette_handler.get_pipette(mount).is_high_speed_pipette(): + self._backend.update_constraints_for_emulsifying_pipette( + mount, self.gantry_load + ) + + def get_pressure_sensor_available(self, mount: OT3Mount) -> bool: + pip_axis = Axis.of_main_tool_actuator(mount) + return self._backend.get_pressure_sensor_available(pip_axis) + + def _set_pressure_sensor_available( + self, + mount: OT3Mount, + instrument_config: pipette_definition.PipetteConfigurations, + ) -> None: + pressure_sensor_available = ( + "pressure" in instrument_config.available_sensors.sensors + ) + pip_axis = Axis.of_main_tool_actuator(mount) + self._backend.set_pressure_sensor_available( + pipette_axis=pip_axis, available=pressure_sensor_available + ) + async def cache_gripper(self, instrument_data: AttachedGripper) -> bool: """Set up gripper based on scanned information.""" grip_cal = load_gripper_calibration_offset(instrument_data.get("id")) @@ -776,12 +810,14 @@ async def _update_position_estimation( """ Function to update motor estimation for a set of axes """ + await self._backend.update_motor_status() - if axes: - checked_axes = [ax for ax in axes if ax in Axis] - else: - checked_axes = [ax for ax in Axis] - await self._backend.update_motor_estimation(checked_axes) + if axes is None: + axes = [ax for ax in Axis] + + axes = [ax for ax in axes if self._backend.axis_is_present(ax)] + + await self._backend.update_motor_estimation(axes) # Global actions API def pause(self, pause_type: PauseType) -> None: diff --git a/api/src/opentrons/hardware_control/protocols/position_estimator.py b/api/src/opentrons/hardware_control/protocols/position_estimator.py index 04d551020c3..fc4e1521a89 100644 --- a/api/src/opentrons/hardware_control/protocols/position_estimator.py +++ b/api/src/opentrons/hardware_control/protocols/position_estimator.py @@ -10,7 +10,7 @@ async def update_axis_position_estimations(self, axes: Sequence[Axis]) -> None: """Update the specified axes' position estimators from their encoders. This will allow these axes to make a non-home move even if they do not currently have - a position estimation (unless there is no tracked poition from the encoders, as would be + a position estimation (unless there is no tracked position from the encoders, as would be true immediately after boot). Axis encoders have less precision than their position estimators. Calling this function will @@ -19,6 +19,8 @@ async def update_axis_position_estimations(self, axes: Sequence[Axis]) -> None: This function updates only the requested axes. If other axes have bad position estimation, moves that require those axes or attempts to get the position of those axes will still fail. + Axes that are not currently available (like a plunger for a pipette that is not connected) + will be ignored. """ ... diff --git a/api/src/opentrons/hardware_control/types.py b/api/src/opentrons/hardware_control/types.py index 62265afffcc..bc32431d2a5 100644 --- a/api/src/opentrons/hardware_control/types.py +++ b/api/src/opentrons/hardware_control/types.py @@ -625,6 +625,8 @@ class GripperJawState(enum.Enum): #: the gripper is actively force-control gripping something HOLDING = enum.auto() #: the gripper is in position-control mode + STOPPED = enum.auto() + #: the gripper has been homed before but is stopped now class InstrumentProbeType(enum.Enum): diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index 2f35bb46764..41a061f5a94 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -30,7 +30,16 @@ ) from .disposal_locations import TrashBin, WasteChute from ._liquid import Liquid, LiquidClass -from ._types import OFF_DECK +from ._types import ( + OFF_DECK, + PLUNGER_BLOWOUT, + PLUNGER_TOP, + PLUNGER_BOTTOM, + PLUNGER_DROPTIP, + ASPIRATE_ACTION, + DISPENSE_ACTION, + BLOWOUT_ACTION, +) from ._nozzle_layout import ( COLUMN, PARTIAL_COLUMN, @@ -69,12 +78,22 @@ "Liquid", "LiquidClass", "Parameters", + # Partial Tip types "COLUMN", "PARTIAL_COLUMN", "SINGLE", "ROW", "ALL", + # Deck location types "OFF_DECK", + # Pipette plunger types + "PLUNGER_BLOWOUT", + "PLUNGER_TOP", + "PLUNGER_BOTTOM", + "PLUNGER_DROPTIP", + "ASPIRATE_ACTION", + "DISPENSE_ACTION", + "BLOWOUT_ACTION", "RuntimeParameterRequiredError", "CSVParameter", # For internal Opentrons use only: diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index 06a23a29eb8..5aaed51edbe 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from numpy import interp -from typing import Optional, Dict, Sequence, Union, Tuple +from typing import Optional, Dict, Sequence, Tuple from opentrons_shared_data.liquid_classes.liquid_class_definition import ( AspirateProperties as SharedDataAspirateProperties, @@ -23,12 +23,9 @@ class LiquidHandlingPropertyByVolume: - def __init__(self, properties_by_volume: Dict[str, float]) -> None: - self._default = properties_by_volume["default"] + def __init__(self, by_volume_property: Sequence[Tuple[float, float]]) -> None: self._properties_by_volume: Dict[float, float] = { - float(volume): value - for volume, value in properties_by_volume.items() - if volume != "default" + float(volume): value for volume, value in by_volume_property } # Volumes need to be sorted for proper interpolation of non-defined volumes, and the # corresponding values need to be in the same order for them to be interpolated correctly @@ -36,18 +33,17 @@ def __init__(self, properties_by_volume: Dict[str, float]) -> None: self._sorted_values: Tuple[float, ...] = () self._sort_volume_and_values() - @property - def default(self) -> float: - """Get the default value not associated with any volume for this property.""" - return self._default - - def as_dict(self) -> Dict[Union[float, str], float]: + def as_dict(self) -> Dict[float, float]: """Get a dictionary representation of all set volumes and values along with the default.""" - return self._properties_by_volume | {"default": self._default} + return self._properties_by_volume def get_for_volume(self, volume: float) -> float: """Get a value by volume for this property. Volumes not defined will be interpolated between set volumes.""" validated_volume = validation.ensure_positive_float(volume) + if len(self._properties_by_volume) == 0: + raise ValueError( + "No properties found for any volumes. Cannot interpolate for the given volume." + ) try: return self._properties_by_volume[validated_volume] except KeyError: @@ -66,9 +62,9 @@ def delete_for_volume(self, volume: float) -> None: """Remove an existing volume and value from the property.""" try: del self._properties_by_volume[volume] - self._sort_volume_and_values() except KeyError: raise KeyError(f"No value set for volume {volume} uL") + self._sort_volume_and_values() def _sort_volume_and_values(self) -> None: """Sort volume in increasing order along with corresponding values in matching order.""" diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py index 9890e29c2bc..0e73405b3b7 100644 --- a/api/src/opentrons/protocol_api/_types.py +++ b/api/src/opentrons/protocol_api/_types.py @@ -17,3 +17,27 @@ class OffDeckType(enum.Enum): See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`. """ + + +class PlungerPositionTypes(enum.Enum): + PLUNGER_TOP = "top" + PLUNGER_BOTTOM = "bottom" + PLUNGER_BLOWOUT = "blow_out" + PLUNGER_DROPTIP = "drop_tip" + + +PLUNGER_TOP: Final = PlungerPositionTypes.PLUNGER_TOP +PLUNGER_BOTTOM: Final = PlungerPositionTypes.PLUNGER_BOTTOM +PLUNGER_BLOWOUT: Final = PlungerPositionTypes.PLUNGER_BLOWOUT +PLUNGER_DROPTIP: Final = PlungerPositionTypes.PLUNGER_DROPTIP + + +class PipetteActionTypes(enum.Enum): + ASPIRATE_ACTION = "aspirate" + DISPENSE_ACTION = "dispense" + BLOWOUT_ACTION = "blowout" + + +ASPIRATE_ACTION: Final = PipetteActionTypes.ASPIRATE_ACTION +DISPENSE_ACTION: Final = PipetteActionTypes.DISPENSE_ACTION +BLOWOUT_ACTION: Final = PipetteActionTypes.BLOWOUT_ACTION diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 2f172c8cda2..8fc707541f0 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -32,6 +32,9 @@ from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.errors.exceptions import ( + UnsupportedHardwareCommand, +) from opentrons.protocol_api._nozzle_layout import NozzleLayout from . import overlap_versions, pipette_movement_conflict @@ -85,6 +88,13 @@ def __init__( self._liquid_presence_detection = bool( self._engine_client.state.pipettes.get_liquid_presence_detection(pipette_id) ) + if ( + self._liquid_presence_detection + and not self._pressure_supported_by_pipette() + ): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) @property def pipette_id(self) -> str: @@ -859,6 +869,11 @@ def retract(self) -> None: z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) self._engine_client.execute_command(cmd.HomeParams(axes=[z_axis])) + def _pressure_supported_by_pipette(self) -> bool: + return self._engine_client.state.pipettes.get_pipette_supports_pressure( + self.pipette_id + ) + def detect_liquid_presence(self, well_core: WellCore, loc: Location) -> bool: labware_id = well_core.labware_id well_name = well_core.get_name() diff --git a/api/src/opentrons/protocol_api/core/engine/robot.py b/api/src/opentrons/protocol_api/core/engine/robot.py index 477f1968c5a..df80917e091 100644 --- a/api/src/opentrons/protocol_api/core/engine/robot.py +++ b/api/src/opentrons/protocol_api/core/engine/robot.py @@ -1,13 +1,16 @@ -from typing import Optional, Dict +from typing import Optional, Dict, Union from opentrons.hardware_control import SyncHardwareAPI from opentrons.types import Mount, MountType, Point, AxisType, AxisMapType +from opentrons_shared_data.pipette import types as pip_types +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_engine.types import DeckPoint, MotorAxis from opentrons.protocol_api.core.robot import AbstractRobot + _AXIS_TYPE_TO_MOTOR_AXIS = { AxisType.X: MotorAxis.X, AxisType.Y: MotorAxis.Y, @@ -39,12 +42,57 @@ def __init__( def _convert_to_engine_mount(self, axis_map: AxisMapType) -> Dict[MotorAxis, float]: return {_AXIS_TYPE_TO_MOTOR_AXIS[ax]: dist for ax, dist in axis_map.items()} - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[pip_types.PipetteNameType]: """Get the pipette attached to the given mount.""" - engine_mount = MountType[mount.name] + if isinstance(mount, Mount): + engine_mount = MountType[mount.name] + else: + if mount.lower() == "right": + engine_mount = MountType.RIGHT + else: + engine_mount = MountType.LEFT maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) return maybe_pipette.pipetteName if maybe_pipette else None + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + return 0.0 + return self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, position_name.value + ) + + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + engine_mount = MountType[mount.name] + maybe_pipette = self._engine_client.state.pipettes.get_by_mount(engine_mount) + if not maybe_pipette: + raise RuntimeError( + f"Cannot load plunger position as no pipette is attached to {mount}" + ) + convert_volume = ( + self._engine_client.state.pipettes.lookup_volume_to_mm_conversion( + maybe_pipette.id, volume, action.value + ) + ) + plunger_bottom = ( + self._engine_client.state.pipettes.lookup_plunger_position_name( + maybe_pipette.id, "bottom" + ) + ) + mm = volume / convert_volume + if robot_type == "OT-2 Standard": + position = plunger_bottom + mm + else: + position = plunger_bottom - mm + return round(position, 6) + def move_to(self, mount: Mount, destination: Point, speed: Optional[float]) -> None: engine_mount = MountType[mount.name] engine_destination = DeckPoint( diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index d17ab43dd4f..f110bde928d 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -260,6 +260,10 @@ def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: def get_liquid_presence_detection(self) -> bool: ... + @abstractmethod + def _pressure_supported_by_pipette(self) -> bool: + ... + @abstractmethod def set_liquid_presence_detection(self, enable: bool) -> None: ... 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 90a8a05c6da..76d49b40557 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 @@ -586,6 +586,9 @@ def liquid_probe_without_recovery( """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" + def _pressure_supported_by_pipette(self) -> bool: + return False + def nozzle_configuration_valid_for_lld(self) -> bool: """Check if the nozzle configuration currently supports LLD.""" return False 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 e672a6fe839..d0b95ed82ca 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 @@ -37,7 +37,6 @@ class LegacyProtocolCore( LegacyInstrumentCore, LegacyLabwareCore, legacy_module_core.LegacyModuleCore, - # None, ] ): def __init__( 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 66c33aae511..f55bf05c447 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 @@ -504,6 +504,9 @@ def liquid_probe_without_recovery( """This will never be called because it was added in API 2.20.""" assert False, "liquid_probe_without_recovery only supported in API 2.20 & later" + def _pressure_supported_by_pipette(self) -> bool: + return False + def nozzle_configuration_valid_for_lld(self) -> bool: """Check if the nozzle configuration currently supports LLD.""" return False diff --git a/api/src/opentrons/protocol_api/core/robot.py b/api/src/opentrons/protocol_api/core/robot.py index 7eade528413..95def3e17f3 100644 --- a/api/src/opentrons/protocol_api/core/robot.py +++ b/api/src/opentrons/protocol_api/core/robot.py @@ -1,12 +1,28 @@ from abc import abstractmethod, ABC -from typing import Optional +from typing import Optional, Union from opentrons.types import AxisMapType, Mount, Point +from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons.protocol_api._types import PlungerPositionTypes, PipetteActionTypes class AbstractRobot(ABC): @abstractmethod - def get_pipette_type_from_engine(self, mount: Mount) -> Optional[str]: + def get_pipette_type_from_engine( + self, mount: Union[Mount, str] + ) -> Optional[PipetteNameType]: + ... + + @abstractmethod + def get_plunger_position_from_volume( + self, mount: Mount, volume: float, action: PipetteActionTypes, robot_type: str + ) -> float: + ... + + @abstractmethod + def get_plunger_position_from_name( + self, mount: Mount, position_name: PlungerPositionTypes + ) -> float: ... @abstractmethod diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 3aee270e9a7..7cc2d43bac2 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -6,6 +6,7 @@ CommandPreconditionViolated, CommandParameterLimitViolated, UnexpectedTipRemovalError, + UnsupportedHardwareCommand, ) from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict @@ -259,6 +260,7 @@ def aspirate( and self._core.nozzle_configuration_valid_for_lld() and self._core.get_current_volume() == 0 ): + self._raise_if_pressure_not_supported_by_pipette() self.require_liquid_presence(well=well) with publisher.publish_context( @@ -512,6 +514,8 @@ def mix( ``pipette.mix(1, location=wellplate['A1'])`` is a valid call, but ``pipette.mix(1, wellplate['A1'])`` is not. + .. versionchanged:: 2.21 + Does not repeatedly check for liquid presence. """ _log.debug( "mixing {}uL with {} repetitions in {} at rate={}".format( @@ -1703,6 +1707,8 @@ def liquid_presence_detection(self) -> bool: @liquid_presence_detection.setter @requires_version(2, 20) def liquid_presence_detection(self, enable: bool) -> None: + if enable: + self._raise_if_pressure_not_supported_by_pipette() self._core.set_liquid_presence_detection(enable) @property @@ -2139,6 +2145,7 @@ def detect_liquid_presence(self, well: labware.Well) -> bool: .. note:: The pressure sensors for the Flex 8-channel pipette are on channels 1 and 8 (positions A1 and H1). For the Flex 96-channel pipette, the pressure sensors are on channels 1 and 96 (positions A1 and H12). Other channels on multi-channel pipettes do not have sensors and cannot detect liquid. """ + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() return self._core.detect_liquid_presence(well._core, loc) @@ -2151,6 +2158,7 @@ def require_liquid_presence(self, well: labware.Well) -> None: .. note:: The pressure sensors for the Flex 8-channel pipette are on channels 1 and 8 (positions A1 and H1). For the Flex 96-channel pipette, the pressure sensors are on channels 1 and 96 (positions A1 and H12). Other channels on multi-channel pipettes do not have sensors and cannot detect liquid. """ + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() self._core.liquid_probe_with_recovery(well._core, loc) @@ -2164,7 +2172,7 @@ def measure_liquid_height(self, well: labware.Well) -> float: This is intended for Opentrons internal use only and is not a guaranteed API. """ - + self._raise_if_pressure_not_supported_by_pipette() loc = well.top() height = self._core.liquid_probe_without_recovery(well._core, loc) return height @@ -2185,6 +2193,12 @@ def _raise_if_configuration_not_supported_by_pipette( ) # SINGLE, QUADRANT and ALL are supported by all pipettes + def _raise_if_pressure_not_supported_by_pipette(self) -> None: + if not self._core._pressure_supported_by_pipette(): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) + def _handle_aspirate_target( self, target: validation.ValidTarget ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]: diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index 7beab69c53f..8890981e32a 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -581,7 +581,7 @@ def set_block_temperature( individual well of the loaded labware, in µL. If not specified, the default is 25 µL. - .. note: + .. note:: If ``hold_time_minutes`` and ``hold_time_seconds`` are not specified, the Thermocycler will proceed to the next command @@ -605,7 +605,7 @@ def set_lid_temperature(self, temperature: float) -> None: :param temperature: A value between 37 and 110, representing the target temperature in °C. - .. note: + .. note:: The Thermocycler will proceed to the next command immediately after ``temperature`` has been reached. @@ -635,13 +635,13 @@ def execute_profile( individual well of the loaded labware, in µL. If not specified, the default is 25 µL. - .. note: + .. note:: Unlike with :py:meth:`set_block_temperature`, either or both of ``hold_time_minutes`` and ``hold_time_seconds`` must be defined and for each step. - .. note: + .. note:: Before API Version 2.21, Thermocycler profiles run with this command would be listed in the app as having a number of repetitions equal to @@ -991,7 +991,7 @@ class MagneticBlockContext(ModuleContext): class AbsorbanceReaderContext(ModuleContext): - """An object representing a connected Absorbance Reader Module. + """An object representing a connected Absorbance Plate Reader Module. It should not be instantiated directly; instead, it should be created through :py:meth:`.ProtocolContext.load_module`. @@ -1009,17 +1009,21 @@ def serial_number(self) -> str: @requires_version(2, 21) def close_lid(self) -> None: - """Close the lid of the Absorbance Reader.""" + """Use the Flex Gripper to close the lid of the Absorbance Plate Reader. + + You must call this method before initializing the reader, even if the reader was + in the closed position at the start of the protocol. + """ self._core.close_lid() @requires_version(2, 21) def open_lid(self) -> None: - """Open the lid of the Absorbance Reader.""" + """Use the Flex Gripper to open the lid of the Absorbance Plate Reader.""" self._core.open_lid() @requires_version(2, 21) def is_lid_on(self) -> bool: - """Return ``True`` if the Absorbance Reader's lid is currently closed.""" + """Return ``True`` if the Absorbance Plate Reader's lid is currently closed.""" return self._core.is_lid_on() @requires_version(2, 21) @@ -1029,19 +1033,28 @@ def initialize( wavelengths: List[int], reference_wavelength: Optional[int] = None, ) -> None: - """Take a zero reading on the Absorbance Plate Reader Module. + """Prepare the Absorbance Plate Reader to read a plate. + + See :ref:`absorbance-initialization` for examples. :param mode: Either ``"single"`` or ``"multi"``. - - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses - one sample wavelength and an optional reference wavelength. - - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses - a list of up to six sample wavelengths. - :param wavelengths: A list of wavelengths, in mm, to measure. - - Must contain only one item when initializing a single measurement. - - Must contain one to six items when initializing a multiple measurement. - :param reference_wavelength: An optional reference wavelength, in mm. Cannot be - used with multiple measurements. + - In single measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses + one sample wavelength and an optional reference wavelength. + - In multiple measurement mode, :py:meth:`.AbsorbanceReaderContext.read` uses + a list of up to six sample wavelengths. + :param wavelengths: A list of wavelengths, in nm, to measure. + + - In the default hardware configuration, each wavelength must be one of + ``450`` (blue), ``562`` (green), ``600`` (orange), or ``650`` (red). In + custom hardware configurations, the module may accept other integers + between 350 and 1000. + - The list must contain only one item when initializing a single measurement. + - The list can contain one to six items when initializing a multiple measurement. + :param reference_wavelength: An optional reference wavelength, in nm. If provided, + :py:meth:`.AbsorbanceReaderContext.read` will read at the reference + wavelength and then subtract the reference wavelength values from the + measurement wavelength values. Can only be used with single measurements. """ self._core.initialize( mode, wavelengths, reference_wavelength=reference_wavelength @@ -1051,16 +1064,33 @@ def initialize( def read( self, export_filename: Optional[str] = None ) -> Dict[int, Dict[str, float]]: - """Initiate read on the Absorbance Reader. + """Read a plate on the Absorbance Plate Reader. + + This method always returns a dictionary of measurement data. It optionally will + save a CSV file of the results to the Flex filesystem, which you can access from + the Recent Protocol Runs screen in the Opentrons App. These files are `only` saved + if you specify ``export_filename``. + + In simulation, the values for each well key in the dictionary are set to zero, and + no files are written. + + .. note:: + + Avoid divide-by-zero errors when simulating and using the results of this + method later in the protocol. If you divide by any of the measurement + values, use :py:meth:`.ProtocolContext.is_simulating` to use alternate dummy + data or skip the division step. - Returns a dictionary of wavelengths to dictionary of values ordered by well name. + :param export_filename: An optional file basename. If provided, this method + will write a CSV file for each measurement in the read operation. File + names will use the value of this parameter, the measurement wavelength + supplied in :py:meth:`~.AbsorbanceReaderContext.initialize`, and a + ``.csv`` extension. For example, when reading at wavelengths 450 and 562 + with ``export_filename="my_data"``, there will be two output files: + ``my_data_450.csv`` and ``my_data_562.csv``. - :param export_filename: Optional, if a filename is provided a CSV file will be saved - as a result of the read action containing measurement data. The filename will - be modified to include the wavelength used during measurement. If multiple - measurements are taken, then a file will be generated for each wavelength provided. + See :ref:`absorbance-csv` for information on working with these CSV files. - Example: If `export_filename="my_data"` and wavelengths 450 and 531 are used during - measurement, the output files will be "my_data_450.csv" and "my_data_531.csv". + :returns: A dictionary of wavelengths to dictionary of values ordered by well name. """ return self._core.read(filename=export_filename) diff --git a/api/src/opentrons/protocol_api/robot_context.py b/api/src/opentrons/protocol_api/robot_context.py index 272330e1664..5b0e578f9bb 100644 --- a/api/src/opentrons/protocol_api/robot_context.py +++ b/api/src/opentrons/protocol_api/robot_context.py @@ -19,6 +19,7 @@ from .core.common import ProtocolCore, RobotCore from .module_contexts import ModuleContext from .labware import Labware +from ._types import PipetteActionTypes, PlungerPositionTypes class HardwareManager(NamedTuple): @@ -200,14 +201,43 @@ def axis_coordinates_for( raise TypeError("You must specify a location to move to.") def plunger_coordinates_for_volume( - self, mount: Union[Mount, str], volume: float - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], volume: float, action: PipetteActionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from volume. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + + pipette_position = self._core.get_plunger_position_from_volume( + mount, volume, action, self._protocol_core.robot_type + ) + return {pipette_axis: pipette_position} def plunger_coordinates_for_named_position( - self, mount: Union[Mount, str], position_name: str - ) -> None: - raise NotImplementedError() + self, mount: Union[Mount, str], position_name: PlungerPositionTypes + ) -> AxisMapType: + """ + Build a :py:class:`.types.AxisMapType` for a pipette plunger motor from position_name. + + """ + pipette_name = self._core.get_pipette_type_from_engine(mount) + if not pipette_name: + raise ValueError( + f"Expected a pipette to be attached to provided mount {mount}" + ) + mount = validation.ensure_mount_for_pipette(mount, pipette_name) + pipette_axis = AxisType.plunger_axis_for_mount(mount) + pipette_position = self._core.get_plunger_position_from_name( + mount, position_name + ) + return {pipette_axis: pipette_position} def build_axis_map(self, axis_map: StringAxisMap) -> AxisMapType: """Take in a :py:class:`.types.StringAxisMap` and output a :py:class:`.types.AxisMapType`. diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 25599189916..7efaef7199d 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -57,6 +57,8 @@ ModuleModel, ModuleDefinition, Liquid, + LiquidClassRecord, + LiquidClassRecordWithId, AllNozzleLayoutConfiguration, SingleNozzleLayoutConfiguration, RowNozzleLayoutConfiguration, @@ -122,6 +124,8 @@ "ModuleModel", "ModuleDefinition", "Liquid", + "LiquidClassRecord", + "LiquidClassRecordWithId", "AllNozzleLayoutConfiguration", "SingleNozzleLayoutConfiguration", "RowNozzleLayoutConfiguration", diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py index 4b28154ed17..458225ad1bb 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/initialize.py @@ -10,6 +10,7 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence +from ...errors import InvalidWavelengthError if TYPE_CHECKING: from opentrons.protocol_engine.state.state import StateView @@ -69,30 +70,41 @@ async def execute(self, params: InitializeParams) -> SuccessData[InitializeResul unsupported_wavelengths = sample_wavelengths.difference( supported_wavelengths ) + sample_wl_str = ", ".join([str(w) + "nm" for w in sample_wavelengths]) + supported_wl_str = ", ".join([str(w) + "nm" for w in supported_wavelengths]) + unsupported_wl_str = ", ".join( + [str(w) + "nm" for w in unsupported_wavelengths] + ) if unsupported_wavelengths: - raise ValueError(f"Unsupported wavelengths: {unsupported_wavelengths}") + raise InvalidWavelengthError( + f"Unsupported wavelengths: {unsupported_wl_str}. " + f" Use one of {supported_wl_str} instead." + ) if params.measureMode == "single": if sample_wavelengths_len != 1: raise ValueError( - f"single requires one sample wavelength, provided {sample_wavelengths}" + f"Measure mode `single` requires one sample wavelength," + f" {sample_wl_str} provided instead." ) if ( reference_wavelength is not None and reference_wavelength not in supported_wavelengths ): - raise ValueError( - f"Reference wavelength {reference_wavelength} not supported {supported_wavelengths}" + raise InvalidWavelengthError( + f"Reference wavelength {reference_wavelength}nm is not supported." + f" Use one of {supported_wl_str} instead." ) if params.measureMode == "multi": if sample_wavelengths_len < 1 or sample_wavelengths_len > 6: raise ValueError( - f"multi requires 1-6 sample wavelengths, provided {sample_wavelengths}" + f"Measure mode `multi` requires 1-6 sample wavelengths," + f" {sample_wl_str} provided instead." ) if reference_wavelength is not None: - raise RuntimeError( - "Reference wavelength cannot be used with multi mode." + raise ValueError( + "Reference wavelength cannot be used with Measure mode `multi`." ) await abs_reader.set_sample_wavelength( diff --git a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py index 8743fd1383b..1ca848858b6 100644 --- a/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py +++ b/api/src/opentrons/protocol_engine/commands/absorbance_reader/read.py @@ -80,6 +80,10 @@ async def execute( # noqa: C901 raise CannotPerformModuleAction( "Cannot perform Read action on Absorbance Reader without calling `.initialize(...)` first." ) + if abs_reader_substate.is_lid_on is False: + raise CannotPerformModuleAction( + "Cannot perform Read action on Absorbance Reader with the lid open. Try calling `.close_lid()` first." + ) # TODO: we need to return a file ID and increase the file count even when a moduel is not attached if ( diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 7fc7b62dc45..1f89c9c5d74 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -84,7 +84,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( pipette_id=params.pipetteId, ) - if not ready_to_aspirate: raise PipetteNotReadyToAspirateError( "Pipette cannot aspirate in place because of a previous blow out." diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index e47ae9f3a37..c009f314afb 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -184,7 +184,9 @@ class BaseCommand( ) error: Union[ _ErrorT, - # ErrorOccurrence here is for undefined errors not captured by _ErrorT. + # ErrorOccurrence here is a catch-all for undefined errors not captured by + # _ErrorT, or defined errors that don't parse into _ErrorT because, for example, + # they are from an older software version that was missing some fields. ErrorOccurrence, None, ] = Field( diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 18c90360c42..4faee3d5e2f 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -147,6 +147,13 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: error=exception, ) ], + errorInfo={ + "retryLocation": ( + move_result.public.position.x, + move_result.public.position.y, + move_result.public.position.z, + ) + }, ) return DefinedErrorData( public=error, @@ -168,7 +175,11 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: ) -class DropTip(BaseCommand[DropTipParams, DropTipResult, ErrorOccurrence]): +class DropTip( + BaseCommand[ + DropTipParams, DropTipResult, TipPhysicallyAttachedError | StallOrCollisionError + ] +): """Drop tip command model.""" commandType: DropTipCommandType = "dropTip" diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py index 0f98b32ff58..8687382b53f 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py @@ -18,7 +18,7 @@ from ..state import update_types if TYPE_CHECKING: - from ..execution import TipHandler + from ..execution import TipHandler, GantryMover DropTipInPlaceCommandType = Literal["dropTipInPlace"] @@ -57,14 +57,19 @@ def __init__( self, tip_handler: TipHandler, model_utils: ModelUtils, + gantry_mover: GantryMover, **kwargs: object, ) -> None: self._tip_handler = tip_handler self._model_utils = model_utils + self._gantry_mover = gantry_mover async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: """Drop a tip using the requested pipette.""" state_update = update_types.StateUpdate() + + retry_location = await self._gantry_mover.get_position(params.pipetteId) + try: await self._tip_handler.drop_tip( pipette_id=params.pipetteId, home_after=params.homeAfter @@ -85,6 +90,7 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: error=exception, ) ], + errorInfo={"retryLocation": retry_location}, ) return DefinedErrorData( public=error, @@ -100,7 +106,7 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: class DropTipInPlace( - BaseCommand[DropTipInPlaceParams, DropTipInPlaceResult, ErrorOccurrence] + BaseCommand[DropTipInPlaceParams, DropTipInPlaceResult, TipPhysicallyAttachedError] ): """Drop tip in place command model.""" diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index b99e6ac11b1..1bf58e8be26 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -17,6 +17,7 @@ from opentrons.types import MountType from opentrons_shared_data.errors.exceptions import ( PipetteLiquidNotFoundError, + UnsupportedHardwareCommand, ) from ..types import DeckPoint @@ -119,6 +120,14 @@ async def _execute_common( pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName + if ( + "pressure" + not in state_view.pipettes.get_config(pipette_id).available_sensors.sensors + ): + raise UnsupportedHardwareCommand( + "Pressure sensor not available for this pipette" + ) + if not state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id): raise TipNotAttachedError( "Either the front right or back left nozzle must have a tip attached to probe liquid height." diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 101d9f2e02c..af8723a5bba 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -87,7 +87,7 @@ class TipPhysicallyMissingError(ErrorOccurrence): isDefined: bool = True errorType: Literal["tipPhysicallyMissing"] = "tipPhysicallyMissing" errorCode: str = ErrorCodes.TIP_PICKUP_FAILED.value.code - detail: str = "No tip detected." + detail: str = "No Tip Detected" _ExecuteReturn = Union[ diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index ee69a3e3764..0292b51eee1 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -72,7 +72,12 @@ class BaseLiquidHandlingResult(BaseModel): class ErrorLocationInfo(TypedDict): - """Holds a retry location for in-place error recovery.""" + """Holds a retry location for in-place error recovery. + + This is appropriate to pass to a `moveToCoordinates` command, + assuming the pipette has not been configured with a different nozzle layout + in the meantime. + """ retryLocation: Tuple[float, float, float] @@ -126,6 +131,8 @@ class TipPhysicallyAttachedError(ErrorOccurrence): errorCode: str = ErrorCodes.TIP_DROP_FAILED.value.code detail: str = ErrorCodes.TIP_DROP_FAILED.value.detail + errorInfo: ErrorLocationInfo + async def prepare_for_aspirate( pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index aa11555954d..c69cea29243 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -129,9 +129,14 @@ async def execute( module.id ) - # NOTE: When the estop is pressed, the gantry loses position, - # so the robot needs to home x, y to sync. - await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G, Axis.X, Axis.Y]) + # NOTE: When the estop is pressed, the gantry loses position, lets use + # the encoders to sync position. + # Ideally, we'd do a full home, but this command is used when + # the gripper is holding the plate reader, and a full home would + # bang it into the right window. + await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) + await ot3api.engage_axes([Axis.X, Axis.Y]) + await ot3api.update_axis_position_estimations([Axis.X, Axis.Y]) # Place the labware down await self._start_movement(ot3api, definition, location, drop_offset) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py index cf5454db332..ff06b6c22ed 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py @@ -23,7 +23,11 @@ class UpdatePositionEstimatorsParams(BaseModel): """Payload required for an UpdatePositionEstimators command.""" axes: List[MotorAxis] = Field( - ..., description="The axes for which to update the position estimators." + ..., + description=( + "The axes for which to update the position estimators." + " Any axes that are not physically present will be ignored." + ), ) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 2706a4bc862..8148ce132e6 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -55,6 +55,7 @@ InvalidTargetTemperatureError, InvalidBlockVolumeError, InvalidHoldTimeError, + InvalidWavelengthError, CannotPerformModuleAction, PauseNotAllowedError, ResumeFromRecoveryNotAllowedError, @@ -141,6 +142,7 @@ "InvalidBlockVolumeError", "InvalidHoldTimeError", "InvalidLiquidError", + "InvalidWavelengthError", "CannotPerformModuleAction", "ResumeFromRecoveryNotAllowedError", "PauseNotAllowedError", diff --git a/api/src/opentrons/protocol_engine/errors/error_occurrence.py b/api/src/opentrons/protocol_engine/errors/error_occurrence.py index 02bcfb38b62..4141befe9b8 100644 --- a/api/src/opentrons/protocol_engine/errors/error_occurrence.py +++ b/api/src/opentrons/protocol_engine/errors/error_occurrence.py @@ -12,8 +12,6 @@ log = getLogger(__name__) -# TODO(mc, 2021-11-12): flesh this model out with structured error data -# for each error type so client may produce better error messages class ErrorOccurrence(BaseModel): """An occurrence of a specific error during protocol execution.""" @@ -44,8 +42,15 @@ def from_failed( id: str = Field(..., description="Unique identifier of this error occurrence.") createdAt: datetime = Field(..., description="When the error occurred.") + # Our Python should probably always set this to False--if we want it to be True, + # we should probably be using a more specific subclass of ErrorOccurrence anyway. + # However, we can't make this Literal[False], because we want this class to be able + # to act as a catch-all for parsing defined errors that might be missing some + # `errorInfo` fields because they were serialized by older software. isDefined: bool = Field( - default=False, # default=False for database backwards compatibility. + # default=False for database backwards compatibility, so we can parse objects + # serialized before isDefined existed. + default=False, description=dedent( """\ Whether this error is *defined.* diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 7c16156b4bb..563a1fb816d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -786,6 +786,19 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class InvalidWavelengthError(ProtocolEngineError): + """Raised when attempting to set an invalid absorbance wavelength.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a InvalidWavelengthError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class InvalidHoldTimeError(ProtocolEngineError): """An error raised when attempting to set an invalid temperature hold time.""" diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index 7306bc4e4d1..c77a9e1bad2 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -64,6 +64,7 @@ HardwareAxis.Q: MotorAxis.AXIS_96_CHANNEL_CAM, } + # The height of the bottom of the pipette nozzle at home position without any tips. # We rely on this being the same for every OT-3 pipette. # @@ -305,7 +306,6 @@ async def move_mount_to( ) -> Point: """Move the given hardware mount to a waypoint.""" assert len(waypoints) > 0, "Must have at least one waypoint" - log.info(f"Moving mount {mount}") for waypoint in waypoints: log.info(f"The current waypoint moving is {waypoint}") await self._hardware_api.move_to( @@ -340,6 +340,10 @@ async def move_axes( mount, refresh=True ) log.info(f"The current position of the robot is: {current_position}.") + converted_current_position_deck = ( + self._hardware_api.get_deck_from_machine(current_position) + ) + log.info(f"The current position of the robot is: {current_position}.") pos_hw = target_axis_map_from_relative(pos_hw, current_position) log.info( diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index d3998c69bd1..4df6b0d4d77 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -67,6 +67,9 @@ class LoadedStaticPipetteData: back_left_corner_offset: Point front_right_corner_offset: Point pipette_lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: pipette_definition.AvailableSensorDefinition class VirtualPipetteDataProvider: @@ -252,6 +255,7 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_back_left = config.pipette_bounding_box_offsets.back_left_corner pip_front_right = config.pipette_bounding_box_offsets.front_right_corner + plunger_positions = config.plunger_positions_configurations[liquid_class] return LoadedStaticPipetteData( model=str(pipette_model), display_name=config.display_name, @@ -280,6 +284,15 @@ def _get_virtual_pipette_static_config_by_model( # noqa: C901 pip_front_right[0], pip_front_right[1], pip_front_right[2] ), pipette_lld_settings=config.lld_settings, + plunger_positions={ + "top": plunger_positions.top, + "bottom": plunger_positions.bottom, + "blow_out": plunger_positions.blow_out, + "drop_tip": plunger_positions.drop_tip, + }, + shaft_ul_per_mm=config.shaft_ul_per_mm, + available_sensors=config.available_sensors + or pipette_definition.AvailableSensorDefinition(sensors=[]), ) def get_virtual_pipette_static_config( @@ -298,6 +311,11 @@ def get_pipette_static_config( """Get the config for a pipette, given the state/config object from the HW API.""" back_left_offset = pipette_dict["pipette_bounding_box_offsets"].back_left_corner front_right_offset = pipette_dict["pipette_bounding_box_offsets"].front_right_corner + available_sensors = ( + pipette_dict["available_sensors"] + if "available_sensors" in pipette_dict.keys() + else pipette_definition.AvailableSensorDefinition(sensors=[]) + ) return LoadedStaticPipetteData( model=pipette_dict["model"], display_name=pipette_dict["display_name"], @@ -327,6 +345,9 @@ def get_pipette_static_config( front_right_offset[0], front_right_offset[1], front_right_offset[2] ), pipette_lld_settings=pipette_dict["lld_settings"], + plunger_positions=pipette_dict["plunger_positions"], + shaft_ul_per_mm=pipette_dict["shaft_ul_per_mm"], + available_sensors=available_sensors, ) diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index e0f2cef1155..6418f50ee90 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -10,11 +10,15 @@ Mapping, Optional, Tuple, + cast, ) from typing_extensions import assert_never from opentrons_shared_data.pipette import pipette_definition +from opentrons_shared_data.pipette.ul_per_mm import calculate_ul_per_mm +from opentrons_shared_data.pipette.types import UlPerMmAction + from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control import CriticalPoint @@ -99,6 +103,9 @@ class StaticPipetteConfig: bounding_nozzle_offsets: BoundingNozzlesOffsets default_nozzle_map: NozzleMap # todo(mm, 2024-10-14): unused, remove? lld_settings: Optional[Dict[str, Dict[str, float]]] + plunger_positions: Dict[str, float] + shaft_ul_per_mm: float + available_sensors: pipette_definition.AvailableSensorDefinition @dataclasses.dataclass @@ -288,6 +295,9 @@ def _update_pipette_config(self, state_update: update_types.StateUpdate) -> None ), default_nozzle_map=config.nozzle_map, lld_settings=config.pipette_lld_settings, + plunger_positions=config.plunger_positions, + shaft_ul_per_mm=config.shaft_ul_per_mm, + available_sensors=config.available_sensors, ) self._state.flow_rates_by_id[ state_update.pipette_config.pipette_id @@ -753,6 +763,13 @@ def get_pipette_bounds_at_specified_move_to_position( pip_front_left_bound, ) + def get_pipette_supports_pressure(self, pipette_id: str) -> bool: + """Return if this pipette supports a pressure sensor.""" + return ( + "pressure" + in self._state.static_config_by_id[pipette_id].available_sensors.sensors + ) + def get_liquid_presence_detection(self, pipette_id: str) -> bool: """Determine if liquid presence detection is enabled for this pipette.""" try: @@ -772,3 +789,31 @@ def get_nozzle_configuration_supports_lld(self, pipette_id: str) -> bool: ): return False return True + + def lookup_volume_to_mm_conversion( + self, pipette_id: str, volume: float, action: str + ) -> float: + """Get the volumn to mm conversion for a pipette.""" + try: + lookup_volume = self.get_working_volume(pipette_id) + except errors.TipNotAttachedError: + lookup_volume = self.get_maximum_volume(pipette_id) + + pipette_config = self.get_config(pipette_id) + lookup_table_from_config = pipette_config.tip_configuration_lookup_table + try: + tip_settings = lookup_table_from_config[lookup_volume] + except KeyError: + tip_settings = list(lookup_table_from_config.values())[0] + return calculate_ul_per_mm( + volume, + cast(UlPerMmAction, action), + tip_settings, + shaft_ul_per_mm=pipette_config.shaft_ul_per_mm, + ) + + def lookup_plunger_position_name( + self, pipette_id: str, position_name: str + ) -> float: + """Get the plunger position provided for the given pipette id.""" + return self.get_config(pipette_id).plunger_positions[position_name] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 47b15e4eb3b..58e977cc2f4 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -9,7 +9,7 @@ from opentrons_shared_data.robot.types import RobotDefinition from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryPolicy -from opentrons.protocol_engine.types import ModuleOffsetData +from opentrons.protocol_engine.types import LiquidClassRecordWithId, ModuleOffsetData from opentrons.util.change_notifier import ChangeNotifier from ..resources import DeckFixedLabware @@ -156,7 +156,12 @@ def get_summary(self) -> StateSummary: wells=self._wells.get_all(), hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(), files=self._state.files.file_ids, - # TODO(dc): Do we want to just dump all the liquid classes into the summary? + liquidClasses=[ + LiquidClassRecordWithId( + liquidClassId=liquid_class_id, **dict(liquid_class_record) + ) + for liquid_class_id, liquid_class_record in self._liquid_classes.get_all().items() + ], ) diff --git a/api/src/opentrons/protocol_engine/state/state_summary.py b/api/src/opentrons/protocol_engine/state/state_summary.py index 7e47ccbbb37..d6b18613071 100644 --- a/api/src/opentrons/protocol_engine/state/state_summary.py +++ b/api/src/opentrons/protocol_engine/state/state_summary.py @@ -11,6 +11,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, WellInfoSummary, ) @@ -32,3 +33,4 @@ class StateSummary(BaseModel): liquids: List[Liquid] = Field(default_factory=list) wells: List[WellInfoSummary] = Field(default_factory=list) files: List[str] = Field(default_factory=list) + liquidClasses: List[LiquidClassRecordWithId] = Field(default_factory=list) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 1a11a99df86..2a0bbf78c28 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -887,6 +887,15 @@ def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: return hash(dict_to_tuple(self.dict())) +class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): + """A LiquidClassRecord with its ID, for use in summary lists.""" + + liquidClassId: str = Field( + ..., + description="Unique identifier for this liquid class.", + ) + + class SpeedRange(NamedTuple): """Minimum and maximum allowed speeds for a shaking module.""" diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 8339b00f930..5568639f246 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -419,6 +419,21 @@ def get_nozzle_maps(self) -> Mapping[str, NozzleMapInterface]: """Get current nozzle maps keyed by pipette id.""" return self._protocol_engine.state_view.tips.get_pipette_nozzle_maps() + def get_tip_attached(self) -> Dict[str, bool]: + """Get current tip state keyed by pipette id.""" + + def has_tip_attached(pipette_id: str) -> bool: + return ( + self._protocol_engine.state_view.pipettes.get_attached_tip(pipette_id) + is not None + ) + + pipette_ids = ( + pipette.id + for pipette in self._protocol_engine.state_view.pipettes.get_all() + ) + return {pipette_id: has_tip_attached(pipette_id) for pipette_id in pipette_ids} + def set_error_recovery_policy(self, policy: ErrorRecoveryPolicy) -> None: """Create error recovery policy for the run.""" self._protocol_engine.set_error_recovery_policy(policy) diff --git a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py index 6da9a0f7aaf..ff460b48f21 100644 --- a/api/src/opentrons/protocols/parameters/csv_parameter_interface.py +++ b/api/src/opentrons/protocols/parameters/csv_parameter_interface.py @@ -60,7 +60,9 @@ def parse_as_csv( as appropriate. :param detect_dialect: If ``True``, examine the file and try to assign it a - :py:class:`csv.Dialect` to improve parsing behavior. + :py:class:`csv.Dialect` to improve parsing behavior. Set this to ``False`` + when using the file output of :py:meth:`.AbsorbanceReaderContext.read` as + a runtime parameter. :param kwargs: For advanced CSV handling, you can pass any of the `formatting parameters `_ accepted by :py:func:`csv.reader` from the Python standard library. diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index fa57ce0dcd5..1f73d63c8c6 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -292,6 +292,11 @@ def mount_for_axis(cls, axis: "AxisType") -> Mount: } return map_mount_to_axis[axis] + @classmethod + def plunger_axis_for_mount(cls, mount: Mount) -> "AxisType": + map_plunger_axis_mount = {Mount.LEFT: cls.P_L, Mount.RIGHT: cls.P_R} + return map_plunger_axis_mount[mount] + @classmethod def ot2_axes(cls) -> List["AxisType"]: return [ diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index cf8fdd0e97c..e8ca2b059ff 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -804,10 +804,10 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: namespace="test-fixture-2", byPipette=[ ByPipetteSetting( - pipetteModel="p20_single_gen2", + pipetteModel="flex_1channel_50", byTipType=[ ByTipTypeSetting( - tiprack="opentrons_96_tiprack_20ul", + tiprack="opentrons_flex_96_tiprack_50ul", aspirate=AspirateProperties( submerge=Submerge( positionReference=PositionReference.LIQUID_MENISCUS, @@ -821,13 +821,13 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], preWet=True, mix=MixProperties(enable=False), delay=DelayProperties( @@ -845,16 +845,16 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_TOP, offset=Coordinate(x=0, y=0, z=5), speed=100, - airGapByVolume={"default": 2, "5": 3, "10": 4}, + airGapByVolume=[(5.0, 3.0), (10.0, 4.0)], blowout=BlowoutProperties(enable=False), touchTip=TouchTipProperties(enable=False), delay=DelayProperties(enable=False), ), positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), - flowRateByVolume={"default": 50, "10": 40, "20": 30}, + flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], mix=MixProperties(enable=False), - pushOutByVolume={"default": 5, "10": 7, "20": 10}, + pushOutByVolume=[(10.0, 7.0), (20.0, 10.0)], delay=DelayProperties(enable=False), ), multiDispense=None, diff --git a/api/tests/opentrons/drivers/absorbance_reader/test_driver.py b/api/tests/opentrons/drivers/absorbance_reader/test_driver.py index 58552695f44..b4db8d604b2 100644 --- a/api/tests/opentrons/drivers/absorbance_reader/test_driver.py +++ b/api/tests/opentrons/drivers/absorbance_reader/test_driver.py @@ -124,6 +124,36 @@ async def test_driver_get_device_info( mock_interface.get_device_information.assert_called_once() mock_interface.reset_mock() + # Test Device info with updated version format + DEVICE_INFO.sn = "OPTMAA00034" + DEVICE_INFO.version = "8" + + mock_interface.get_device_information.return_value = ( + MockErrorCode.NO_ERROR, + DEVICE_INFO, + ) + + info = await connected_driver.get_device_info() + + assert info == {"serial": "OPTMAA00034", "model": "ABS96", "version": "8"} + mock_interface.get_device_information.assert_called_once() + mock_interface.reset_mock() + + # Test Device info with invalid version format + DEVICE_INFO.sn = "OPTMAA00034" + DEVICE_INFO.version = "asd" + + mock_interface.get_device_information.return_value = ( + MockErrorCode.NO_ERROR, + DEVICE_INFO, + ) + + info = await connected_driver.get_device_info() + + assert info == {"serial": "OPTMAA00034", "model": "ABS96", "version": "v0"} + mock_interface.get_device_information.assert_called_once() + mock_interface.reset_mock() + @pytest.mark.parametrize( "parts_aligned, module_status", 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 1035649b7f5..9c03bed68b2 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_controller.py @@ -374,6 +374,8 @@ async def test_home_execute( **config ) as mock_runner: present_axes = set(ax for ax in axes if controller.axis_is_present(ax)) + controller.set_pressure_sensor_available(Axis.P_L, True) + controller.set_pressure_sensor_available(Axis.P_R, True) # nothing has been homed assert not controller._motor_status @@ -485,6 +487,8 @@ async def test_home_only_present_devices( homed_position = {} controller._position = starting_position + controller.set_pressure_sensor_available(Axis.P_L, True) + controller.set_pressure_sensor_available(Axis.P_R, True) mock_move_group_run.side_effect = move_group_run_side_effect_home(controller, axes) @@ -729,6 +733,9 @@ async def test_liquid_probe( mock_move_group_run.side_effect = probe_move_group_run_side_effect( head_node, tool_node ) + controller._pipettes_to_monitor_pressure = mock.MagicMock( # type: ignore[method-assign] + return_value=[sensor_node_for_mount(mount)] + ) try: await controller.liquid_probe( mount=mount, @@ -1413,3 +1420,34 @@ async def test_controller_move( assert position == expected_pos assert gear_position == gear_position + + +@pytest.mark.parametrize( + argnames=["axes", "pipette_has_sensor"], + argvalues=[[[Axis.P_L, Axis.P_R], True], [[Axis.P_L, Axis.P_R], False]], +) +async def test_pressure_disable( + controller: OT3Controller, + axes: List[Axis], + mock_present_devices: None, + mock_check_overpressure: None, + pipette_has_sensor: bool, +) -> None: + config = {"run.side_effect": move_group_run_side_effect_home(controller, axes)} + with mock.patch( # type: ignore [call-overload] + "opentrons.hardware_control.backends.ot3controller.MoveGroupRunner", + spec=MoveGroupRunner, + **config + ): + with mock.patch.object(controller, "_monitor_overpressure") as monitor: + controller.set_pressure_sensor_available(Axis.P_L, pipette_has_sensor) + controller.set_pressure_sensor_available(Axis.P_R, True) + + await controller.home(axes, GantryLoad.LOW_THROUGHPUT) + + if pipette_has_sensor: + monitor.assert_called_once_with( + [NodeId.pipette_left, NodeId.pipette_right] + ) + else: + monitor.assert_called_once_with([NodeId.pipette_right]) diff --git a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py index 2e650a2c246..d7125cfb027 100644 --- a/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py +++ b/api/tests/opentrons/hardware_control/backends/test_ot3_utils.py @@ -3,7 +3,7 @@ from opentrons_hardware.hardware_control.motion_planning import Move from opentrons.hardware_control.backends import ot3utils from opentrons_hardware.firmware_bindings.constants import NodeId -from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.hardware_control.types import Axis, OT3Mount, OT3AxisKind from numpy import float64 as f64 from opentrons.config import defaults_ot3, types as conf_types @@ -95,6 +95,22 @@ def test_get_system_contraints_for_plunger() -> None: assert updated_contraints[axis].max_acceleration == set_acceleration +@pytest.mark.parametrize(["mount"], [[OT3Mount.LEFT], [OT3Mount.RIGHT]]) +def test_get_system_constraints_for_emulsifying_pipette(mount: OT3Mount) -> None: + set_max_speed = 90 + config = defaults_ot3.build_with_defaults({}) + pipette_ax = Axis.of_main_tool_actuator(mount) + default_pip_max_speed = config.motion_settings.default_max_speed[ + conf_types.GantryLoad.LOW_THROUGHPUT + ][OT3AxisKind.P] + updated_constraints = ot3utils.get_system_constraints_for_emulsifying_pipette( + config.motion_settings, conf_types.GantryLoad.LOW_THROUGHPUT, mount + ) + other_pipette = list(set(Axis.pipette_axes()) - {pipette_ax})[0] + assert updated_constraints[pipette_ax].max_speed == set_max_speed + assert updated_constraints[other_pipette].max_speed == default_pip_max_speed + + @pytest.mark.parametrize( ["moving", "expected"], [ diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index 4c7247e9ec7..2fd3fb4377c 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -2038,23 +2038,36 @@ def set_mock_plunger_configs() -> None: @pytest.mark.parametrize( - "axes", - [[Axis.X], [Axis.X, Axis.Y], [Axis.X, Axis.Y, Axis.P_L], None], + ("axes_in", "axes_present", "expected_axes"), + [ + ([Axis.X, Axis.Y], [Axis.X, Axis.Y], [Axis.X, Axis.Y]), + ([Axis.X, Axis.Y], [Axis.Y, Axis.Z_L], [Axis.Y]), + (None, list(Axis), list(Axis)), + (None, [Axis.Y, Axis.Z_L], [Axis.Y, Axis.Z_L]), + ], ) async def test_update_position_estimation( ot3_hardware: ThreadManager[OT3API], hardware_backend: OT3Simulator, - axes: List[Axis], + axes_in: List[Axis], + axes_present: List[Axis], + expected_axes: List[Axis], ) -> None: + def _axis_is_present(axis: Axis) -> bool: + return axis in axes_present + with patch.object( hardware_backend, "update_motor_estimation", AsyncMock(spec=hardware_backend.update_motor_estimation), - ) as mock_update: - await ot3_hardware._update_position_estimation(axes) - if axes is None: - axes = [ax for ax in Axis] - mock_update.assert_called_once_with(axes) + ) as mock_update, patch.object( + hardware_backend, + "axis_is_present", + Mock(spec=hardware_backend.axis_is_present), + ) as mock_axis_is_present: + mock_axis_is_present.side_effect = _axis_is_present + await ot3_hardware._update_position_estimation(axes_in) + mock_update.assert_called_once_with(expected_axes) async def test_refresh_positions( diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1b8445ed7b2..1caae624377 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -85,7 +85,7 @@ def mock_instrument_core(decoy: Decoy) -> InstrumentCore: """Get a mock instrument implementation core.""" instrument_core = decoy.mock(cls=InstrumentCore) decoy.when(instrument_core.get_mount()).then_return(Mount.LEFT) - + decoy.when(instrument_core._pressure_supported_by_pipette()).then_return(True) # we need to add this for the mock of liquid_presence detection to actually work # this replaces the mock with a a property again instrument_core._liquid_presence_detection = False # type: ignore[attr-defined] @@ -1501,6 +1501,7 @@ def test_mix_no_lpd( mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1516,6 +1517,7 @@ def test_mix_no_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1523,19 +1525,63 @@ def test_mix_no_lpd( subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, - ) - decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) + # Slight differences in dispense push-out logic for 2.14 and 2.15 api levels + if subject.api_version < APIVersion(2, 16): + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=10, + ) + else: + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, + ) + decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=0, ) @@ -1551,6 +1597,7 @@ def test_mix_with_lpd( """It should aspirate/dispense to a well several times and do 1 lpd.""" mock_well = decoy.mock(cls=Well) bottom_location = Location(point=Point(1, 2, 3), labware=mock_well) + top_location = Location(point=Point(3, 2, 1), labware=None) input_location = Location(point=Point(2, 2, 2), labware=None) last_location = Location(point=Point(9, 9, 9), labware=None) @@ -1566,6 +1613,7 @@ def test_mix_with_lpd( mock_validation.validate_location(location=None, last_location=last_location) ).then_return(WellTarget(well=mock_well, location=None, in_place=False)) decoy.when(mock_well.bottom(z=1.0)).then_return(bottom_location) + decoy.when(mock_well.top()).then_return(top_location) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.get_dispense_flow_rate(1.23)).then_return(5.67) decoy.when(mock_instrument_core.has_tip()).then_return(True) @@ -1577,19 +1625,45 @@ def test_mix_with_lpd( subject.liquid_presence_detection = True subject.mix(repetitions=10, volume=10.0, location=input_location, rate=1.23) decoy.verify( - mock_instrument_core.aspirate(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.aspirate( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + ), times=10, ) decoy.verify( - mock_instrument_core.dispense(), # type: ignore[call-arg] - ignore_extra_args=True, - times=10, + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + 0.0, + None, + ), + times=9, + ) + decoy.verify( + mock_instrument_core.dispense( + bottom_location, + mock_well._core, + 10.0, + 1.23, + 5.67, + False, + None, + None, + ), + times=1, ) - decoy.verify( - mock_instrument_core.liquid_probe_with_recovery(), # type: ignore[call-arg] - ignore_extra_args=True, + mock_instrument_core.liquid_probe_with_recovery(mock_well._core, top_location), times=1, ) diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index 463889b3da6..7118080eda0 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -21,9 +21,8 @@ def test_get_for_pipette_and_tip( ) -> None: """It should get the properties for the specified pipette and tip.""" liq_class = LiquidClass.create(minimal_liquid_class_def2) - result = liq_class.get_for("p20_single_gen2", "opentrons_96_tiprack_20ul") + result = liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") assert result.aspirate.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -36,7 +35,7 @@ def test_get_for_raises_for_incorrect_pipette_or_tip( liq_class = LiquidClass.create(minimal_liquid_class_def2) with pytest.raises(ValueError): - liq_class.get_for("p20_single_gen2", "no_such_tiprack") + liq_class.get_for("flex_1channel_50", "no_such_tiprack") with pytest.raises(ValueError): - liq_class.get_for("p300_single", "opentrons_96_tiprack_20ul") + liq_class.get_for("no_such_pipette", "opentrons_flex_96_tiprack_50ul") diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index e1e9b540149..f7033afb5be 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -16,7 +16,7 @@ def test_build_aspirate_settings() -> None: """It should convert the shared data aspirate settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate @@ -32,7 +32,6 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert aspirate_properties.retract.speed == 100 assert aspirate_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -45,7 +44,7 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.position_reference.value == "well-bottom" assert aspirate_properties.offset == Coordinate(x=0, y=0, z=-5) - assert aspirate_properties.flow_rate_by_volume.as_dict() == {"default": 50.0} + assert aspirate_properties.flow_rate_by_volume.as_dict() == {10: 50.0} assert aspirate_properties.pre_wet is True assert aspirate_properties.mix.enabled is True assert aspirate_properties.mix.repetitions == 3 @@ -56,7 +55,7 @@ def test_build_aspirate_settings() -> None: def test_build_single_dispense_settings() -> None: """It should convert the shared data single dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense @@ -75,7 +74,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert single_dispense_properties.retract.speed == 100 assert single_dispense_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -93,7 +91,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.position_reference.value == "well-bottom" assert single_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) assert single_dispense_properties.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } @@ -101,7 +98,6 @@ def test_build_single_dispense_settings() -> None: assert single_dispense_properties.mix.repetitions == 3 assert single_dispense_properties.mix.volume == 15 assert single_dispense_properties.push_out_by_volume.as_dict() == { - "default": 5.0, 10.0: 7.0, 20.0: 10.0, } @@ -111,7 +107,7 @@ def test_build_single_dispense_settings() -> None: def test_build_multi_dispense_settings() -> None: """It should convert the shared data multi dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") + fixture_data = load_shared_data("liquid-class/fixtures/1/fixture_glycerol50.json") liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense @@ -131,7 +127,6 @@ def test_build_multi_dispense_settings() -> None: assert multi_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) assert multi_dispense_properties.retract.speed == 100 assert multi_dispense_properties.retract.air_gap_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, 10.0: 4.0, } @@ -148,16 +143,13 @@ def test_build_multi_dispense_settings() -> None: assert multi_dispense_properties.position_reference.value == "well-bottom" assert multi_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) assert multi_dispense_properties.flow_rate_by_volume.as_dict() == { - "default": 50.0, 10.0: 40.0, 20.0: 30.0, } assert multi_dispense_properties.conditioning_by_volume.as_dict() == { - "default": 10.0, 5.0: 5.0, } assert multi_dispense_properties.disposal_by_volume.as_dict() == { - "default": 2.0, 5.0: 3.0, } assert multi_dispense_properties.delay.enabled is True @@ -174,14 +166,12 @@ def test_build_multi_dispense_settings_none( def test_liquid_handling_property_by_volume() -> None: """It should create a class that can interpolate values and add and delete new points.""" - subject = LiquidHandlingPropertyByVolume({"default": 42, "5": 50, "10.0": 250}) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} - assert subject.default == 42.0 + subject = LiquidHandlingPropertyByVolume([(5.0, 50.0), (10.0, 250.0)]) + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 subject.set_for_volume(volume=7, value=175.5) assert subject.as_dict() == { - "default": 42, 5.0: 50, 10.0: 250, 7.0: 175.5, @@ -189,7 +179,7 @@ def test_liquid_handling_property_by_volume() -> None: assert subject.get_for_volume(7) == 175.5 subject.delete_for_volume(7) - assert subject.as_dict() == {"default": 42, 5.0: 50, 10.0: 250} + assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 with pytest.raises(KeyError, match="No value set for volume"): diff --git a/api/tests/opentrons/protocol_api/test_robot_context.py b/api/tests/opentrons/protocol_api/test_robot_context.py index c1bdfe48c3f..36b94c52b15 100644 --- a/api/tests/opentrons/protocol_api/test_robot_context.py +++ b/api/tests/opentrons/protocol_api/test_robot_context.py @@ -17,6 +17,9 @@ from opentrons.protocol_api.core.common import ProtocolCore, RobotCore from opentrons.protocol_api import RobotContext, ModuleContext from opentrons.protocol_api.deck import Deck +from opentrons_shared_data.pipette.types import PipetteNameType + +from opentrons.protocol_api._types import PipetteActionTypes, PlungerPositionTypes @pytest.fixture @@ -58,7 +61,12 @@ def subject( api_version: APIVersion, ) -> RobotContext: """Get a RobotContext test subject with its dependencies mocked out.""" - decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return(None) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.LEFT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) + decoy.when(mock_core.get_pipette_type_from_engine(Mount.RIGHT)).then_return( + PipetteNameType.P1000_SINGLE_FLEX + ) return RobotContext( core=mock_core, api_version=api_version, protocol_core=mock_protocol ) @@ -176,3 +184,73 @@ def test_get_axes_coordinates_for( """Test `RobotContext.get_axis_coordinates_for`.""" res = subject.axis_coordinates_for(mount, location_to_move) assert res == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "volume", "action", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, 200, PipetteActionTypes.ASPIRATE_ACTION, {AxisType.P_R: 100}), + (Mount.LEFT, 100, PipetteActionTypes.DISPENSE_ACTION, {AxisType.P_L: 100}), + ], +) +def test_plunger_coordinates_for_volume( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + volume: float, + action: PipetteActionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_volume`.""" + decoy.when( + subject._core.get_plunger_position_from_volume( + mount, volume, action, "OT-3 Standard" + ) + ).then_return(100) + + result = subject.plunger_coordinates_for_volume(mount, volume, action) + assert result == expected_axis_map + + +@pytest.mark.parametrize( + argnames=["mount", "position_name", "expected_axis_map"], + argvalues=[ + (Mount.RIGHT, PlungerPositionTypes.PLUNGER_TOP, {AxisType.P_R: 3}), + ( + Mount.RIGHT, + PlungerPositionTypes.PLUNGER_BOTTOM, + {AxisType.P_R: 3}, + ), + ], +) +def test_plunger_coordinates_for_named_position( + decoy: Decoy, + subject: RobotContext, + mount: Mount, + position_name: PlungerPositionTypes, + expected_axis_map: AxisMapType, +) -> None: + """Test `RobotContext.plunger_coordinates_for_named_position`.""" + decoy.when( + subject._core.get_plunger_position_from_name(mount, position_name) + ).then_return(3) + result = subject.plunger_coordinates_for_named_position(mount, position_name) + assert result == expected_axis_map + + +def test_plunger_methods_raise_without_pipette( + mock_core: RobotCore, mock_protocol: ProtocolCore, api_version: APIVersion +) -> None: + """Test that `RobotContext` plunger functions raise without pipette attached.""" + subject = RobotContext( + core=mock_core, api_version=api_version, protocol_core=mock_protocol + ) + with pytest.raises(ValueError): + subject.plunger_coordinates_for_named_position( + Mount.LEFT, PlungerPositionTypes.PLUNGER_TOP + ) + + with pytest.raises(ValueError): + subject.plunger_coordinates_for_volume( + Mount.LEFT, 200, PipetteActionTypes.ASPIRATE_ACTION + ) diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 97de9fb0c48..20bbd2b646c 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -32,7 +32,7 @@ def test_liquid_class_creation_and_property_fetching( assert ( water.get_for( pipette_load_name, tiprack.load_name - ).dispense.flow_rate_by_volume.default + ).dispense.flow_rate_by_volume.get_for_volume(1) == 50 ) assert ( diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py index d237c9e6090..2d8685109ed 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_for_volume.py @@ -22,10 +22,17 @@ ConfigureForVolumeImplementation, ) from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from ..pipette_fixtures import get_default_nozzle_map from opentrons.types import Point +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.mark.parametrize( "data", [ @@ -41,7 +48,10 @@ ], ) async def test_configure_for_volume_implementation( - decoy: Decoy, equipment: EquipmentHandler, data: ConfigureForVolumeParams + decoy: Decoy, + equipment: EquipmentHandler, + data: ConfigureForVolumeParams, + available_sensors: AvailableSensorDefinition, ) -> None: """A ConfigureForVolume command should have an execution implementation.""" subject = ConfigureForVolumeImplementation(equipment=equipment) @@ -63,6 +73,14 @@ async def test_configure_for_volume_implementation( back_left_corner_offset=Point(10, 20, 30), front_right_corner_offset=Point(40, 50, 60), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index b11887a8824..038ea12255b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -307,6 +307,7 @@ async def test_tip_attached_error( id="error-id", createdAt=datetime(year=1, month=2, day=3), wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (111, 222, 333)}, ), state_update=update_types.StateUpdate( pipette_location=update_types.PipetteLocationUpdate( diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py index 9ea78e7dadd..5565ffea88c 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py @@ -14,13 +14,14 @@ DropTipInPlaceImplementation, ) from opentrons.protocol_engine.errors.exceptions import TipAttachedError -from opentrons.protocol_engine.execution import TipHandler +from opentrons.protocol_engine.execution import TipHandler, GantryMover from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state.update_types import ( PipetteTipStateUpdate, StateUpdate, PipetteUnknownFluidUpdate, ) +from opentrons.types import Point @pytest.fixture @@ -35,14 +36,23 @@ def mock_model_utils(decoy: Decoy) -> ModelUtils: return decoy.mock(cls=ModelUtils) +@pytest.fixture +def mock_gantry_mover(decoy: Decoy) -> GantryMover: + """Get a mock GantryMover.""" + return decoy.mock(cls=GantryMover) + + async def test_success( decoy: Decoy, mock_tip_handler: TipHandler, mock_model_utils: ModelUtils, + mock_gantry_mover: GantryMover, ) -> None: """A DropTip command should have an execution implementation.""" subject = DropTipInPlaceImplementation( - tip_handler=mock_tip_handler, model_utils=mock_model_utils + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + gantry_mover=mock_gantry_mover, ) params = DropTipInPlaceParams(pipetteId="abc", homeAfter=False) @@ -68,14 +78,20 @@ async def test_tip_attached_error( decoy: Decoy, mock_tip_handler: TipHandler, mock_model_utils: ModelUtils, + mock_gantry_mover: GantryMover, ) -> None: """A DropTip command should have an execution implementation.""" subject = DropTipInPlaceImplementation( - tip_handler=mock_tip_handler, model_utils=mock_model_utils + tip_handler=mock_tip_handler, + model_utils=mock_model_utils, + gantry_mover=mock_gantry_mover, ) params = DropTipInPlaceParams(pipetteId="abc", homeAfter=False) + decoy.when(await mock_gantry_mover.get_position(pipette_id="abc")).then_return( + Point(9, 8, 7) + ) decoy.when( await mock_tip_handler.drop_tip(pipette_id="abc", home_after=False) ).then_raise(TipAttachedError("Egads!")) @@ -92,6 +108,7 @@ async def test_tip_attached_error( id="error-id", createdAt=datetime(year=1, month=2, day=3), wrappedErrors=[matchers.Anything()], + errorInfo={"retryLocation": (9, 8, 7)}, ), state_update=StateUpdate( pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc") diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py index ab46c4b03e2..34b979901aa 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py +++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py @@ -16,9 +16,20 @@ PipetteLiquidNotFoundError, StallOrCollisionDetectedError, ) +from opentrons_shared_data.pipette.pipette_definition import ( + AvailableSensorDefinition, + SupportedTipsDefinition, +) + +from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.pipettes import ( + StaticPipetteConfig, + BoundingNozzlesOffsets, + PipetteBoundingBoxOffsets, +) from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint @@ -41,6 +52,8 @@ ) from opentrons.protocol_engine.resources.model_utils import ModelUtils +from ..pipette_fixtures import get_default_nozzle_map + EitherImplementationType = Union[ Type[LiquidProbeImplementation], Type[TryLiquidProbeImplementation] ] @@ -49,6 +62,12 @@ EitherResultType = Union[Type[LiquidProbeResult], Type[TryLiquidProbeResult]] +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.fixture( params=[ (LiquidProbeImplementation, LiquidProbeParams, LiquidProbeResult), @@ -108,6 +127,8 @@ async def test_liquid_probe_implementation( params_type: EitherParamsType, result_type: EitherResultType, model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should move to the destination and do a liquid probe there.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) @@ -157,6 +178,41 @@ async def test_liquid_probe_implementation( state_view.pipettes.get_nozzle_configuration_supports_lld("abc") ).then_return(True) + decoy.when(state_view.pipettes.get_config("abc")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) + timestamp = datetime(year=2020, month=1, day=2) decoy.when(model_utils.get_timestamp()).then_return(timestamp) @@ -190,6 +246,8 @@ async def test_liquid_not_found_error( subject: EitherImplementation, params_type: EitherParamsType, model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a liquid not found error if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -212,7 +270,40 @@ async def test_liquid_not_found_error( ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(0) - + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( await movement.move_to_well( pipette_id=pipette_id, @@ -281,6 +372,8 @@ async def test_liquid_probe_tip_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should raise a TipNotAttached error if the state view indicates that.""" pipette_id = "pipette-id" @@ -302,6 +395,40 @@ async def test_liquid_probe_tip_checking( decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_raise( TipNotAttachedError() ) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) with pytest.raises(TipNotAttachedError): await subject.execute(data) @@ -311,6 +438,8 @@ async def test_liquid_probe_plunger_preparedness_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should raise a PipetteNotReadyToAspirate error if the state view indicates that.""" pipette_id = "pipette-id" @@ -329,6 +458,40 @@ async def test_liquid_probe_plunger_preparedness_checking( decoy.when( state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) ).then_return(True) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(None) with pytest.raises(PipetteNotReadyToAspirateError): await subject.execute(data) @@ -339,6 +502,8 @@ async def test_liquid_probe_volume_checking( state_view: StateView, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a TipNotEmptyError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -358,6 +523,40 @@ async def test_liquid_probe_volume_checking( decoy.when( state_view.pipettes.get_aspirated_volume(pipette_id=pipette_id), ).then_return(123) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( state_view.pipettes.get_nozzle_configuration_supports_lld(pipette_id) ).then_return(True) @@ -379,6 +578,8 @@ async def test_liquid_probe_location_checking( movement: MovementHandler, subject: EitherImplementation, params_type: EitherParamsType, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should return a PositionUnkownError if the hardware API indicates that.""" pipette_id = "pipette-id" @@ -395,6 +596,40 @@ async def test_liquid_probe_location_checking( wellLocation=well_location, ) decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id)).then_return(0) + decoy.when(state_view.pipettes.get_config("pipette-id")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( await movement.check_for_valid_position( mount=MountType.LEFT, @@ -415,6 +650,8 @@ async def test_liquid_probe_stall( subject: EitherImplementation, params_type: EitherParamsType, model_utils: ModelUtils, + available_sensors: AvailableSensorDefinition, + supported_tip_fixture: SupportedTipsDefinition, ) -> None: """It should move to the destination and do a liquid probe there.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) @@ -429,6 +666,40 @@ async def test_liquid_probe_stall( decoy.when(state_view.pipettes.get_aspirated_volume(pipette_id="abc")).then_return( 0 ) + decoy.when(state_view.pipettes.get_config("abc")).then_return( + StaticPipetteConfig( + min_volume=1, + max_volume=9001, + channels=1, + model="blah", + display_name="bleh", + serial_number="", + tip_configuration_lookup_table={9001: supported_tip_fixture}, + nominal_tip_overlap={}, + home_position=0, + nozzle_offset_z=0, + bounding_nozzle_offsets=BoundingNozzlesOffsets( + back_left_offset=Point(x=10, y=20, z=30), + front_right_offset=Point(x=40, y=50, z=60), + ), + default_nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + pipette_bounding_box_offsets=PipetteBoundingBoxOffsets( + back_left_corner=Point(x=10, y=20, z=30), + front_right_corner=Point(x=40, y=50, z=60), + front_left_corner=Point(x=10, y=50, z=60), + back_right_corner=Point(x=40, y=20, z=60), + ), + lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, + ) + ) decoy.when( state_view.pipettes.get_nozzle_configuration_supports_lld("abc") ).then_return(True) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index a42bbc4e4d9..a251c6aef1f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -10,6 +10,7 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.robot.types import RobotType +from opentrons_shared_data.pipette.pipette_definition import AvailableSensorDefinition from opentrons.types import MountType, Point from opentrons.protocol_engine.errors import InvalidSpecificationForRobotTypeError @@ -28,6 +29,12 @@ from ..pipette_fixtures import get_default_nozzle_map +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.mark.parametrize( "data", [ @@ -49,6 +56,7 @@ async def test_load_pipette_implementation( equipment: EquipmentHandler, state_view: StateView, data: LoadPipetteParams, + available_sensors: AvailableSensorDefinition, ) -> None: """A LoadPipette command should have an execution implementation.""" subject = LoadPipetteImplementation(equipment=equipment, state_view=state_view) @@ -69,6 +77,14 @@ async def test_load_pipette_implementation( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( @@ -111,6 +127,7 @@ async def test_load_pipette_implementation_96_channel( decoy: Decoy, equipment: EquipmentHandler, state_view: StateView, + available_sensors: AvailableSensorDefinition, ) -> None: """A LoadPipette command should have an execution implementation.""" subject = LoadPipetteImplementation(equipment=equipment, state_view=state_view) @@ -137,6 +154,14 @@ async def test_load_pipette_implementation_96_channel( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) decoy.when( diff --git a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py index 53eb1f5a59e..ef6d79629be 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py +++ b/api/tests/opentrons/protocol_engine/commands/test_verify_tip_presence.py @@ -23,13 +23,13 @@ async def test_verify_tip_presence_implementation( expectedState=TipPresenceStatus.PRESENT, ) - decoy.when( + result = await subject.execute(data) + + assert result == SuccessData(public=VerifyTipPresenceResult()) + decoy.verify( await tip_handler.verify_tip_presence( pipette_id="pipette-id", expected=TipPresenceStatus.PRESENT, + follow_singular_sensor=None, ) - ).then_return(None) - - result = await subject.execute(data) - - assert result == SuccessData(public=VerifyTipPresenceResult()) + ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index 79131994299..da381635ce3 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -37,11 +37,6 @@ async def test_update_position_estimators_implementation( decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( Axis.Y ) - decoy.when( - await ot3_hardware_api.update_axis_position_estimations( - [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] - ) - ).then_return(None) result = await subject.execute(data) 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 b7a020c2d35..39208184754 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -69,6 +69,14 @@ def _make_config(use_virtual_modules: bool) -> Config: ) +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + @pytest.fixture(autouse=True) def patch_mock_pipette_data_provider( decoy: Decoy, @@ -133,6 +141,7 @@ def tip_overlap_versions(request: SubRequest) -> str: def loaded_static_pipette_data( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, target_tip_overlap_data: Dict[str, float], + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> LoadedStaticPipetteData: """Get a pipette config data value object.""" return LoadedStaticPipetteData( @@ -154,6 +163,14 @@ def loaded_static_pipette_data( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 086b3ec297b..ae3d78d2230 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -7,6 +7,7 @@ from opentrons_shared_data.pipette.pipette_definition import ( PipetteBoundingBoxOffsetDefinition, TIP_OVERLAP_VERSION_MAXIMUM, + AvailableSensorDefinition, ) from opentrons.hardware_control.dev_types import PipetteDict @@ -24,6 +25,12 @@ from opentrons.types import Point +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.fixture def subject_instance() -> VirtualPipetteDataProvider: """Instance of a VirtualPipetteDataProvider for test.""" @@ -32,6 +39,7 @@ def subject_instance() -> VirtualPipetteDataProvider: def test_get_virtual_pipette_static_config( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a pipette name.""" result = subject_instance.get_virtual_pipette_static_config( @@ -65,11 +73,20 @@ def test_get_virtual_pipette_static_config( back_left_corner_offset=Point(0, 0, 10.45), front_right_corner_offset=Point(0, 0, 10.45), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -8.5, + "blow_out": -13.0, + "drop_tip": -27.0, + }, + shaft_ul_per_mm=0.785, + available_sensors=AvailableSensorDefinition(sensors=[]), ) def test_configure_virtual_pipette_for_volume( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return an updated config if the liquid class changes.""" result1 = subject_instance.get_virtual_pipette_static_config( @@ -94,6 +111,14 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 71.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, + available_sensors=available_sensors, ) subject_instance.configure_virtual_pipette_for_volume( "my-pipette", 1, result1.model @@ -120,11 +145,20 @@ def test_configure_virtual_pipette_for_volume( back_left_corner_offset=Point(-8.0, -22.0, -259.15), front_right_corner_offset=Point(-8.0, -22.0, -259.15), pipette_lld_settings={"t50": {"minHeight": 1.0, "minVolume": 0.0}}, + plunger_positions={ + "top": 0.0, + "bottom": 61.5, + "blow_out": 76.5, + "drop_tip": 90.5, + }, + shaft_ul_per_mm=0.785, + available_sensors=available_sensors, ) def test_load_virtual_pipette_by_model_string( subject_instance: VirtualPipetteDataProvider, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a pipette model.""" result = subject_instance.get_virtual_pipette_static_config_by_model_string( @@ -149,6 +183,14 @@ def test_load_virtual_pipette_by_model_string( back_left_corner_offset=Point(-16.0, 43.15, 35.52), front_right_corner_offset=Point(16.0, -43.15, 35.52), pipette_lld_settings={}, + plunger_positions={ + "top": 19.5, + "bottom": -14.5, + "blow_out": -19.0, + "drop_tip": -33.4, + }, + shaft_ul_per_mm=9.621, + available_sensors=AvailableSensorDefinition(sensors=[]), ) @@ -193,6 +235,7 @@ def test_load_virtual_pipette_nozzle_layout( @pytest.fixture def pipette_dict( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> PipetteDict: """Get a pipette dict.""" return { @@ -246,6 +289,9 @@ def pipette_dict( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + "plunger_positions": {"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + "shaft_ul_per_mm": 5.0, + "available_sensors": available_sensors, } @@ -263,6 +309,7 @@ def test_get_pipette_static_config( pipette_dict: PipetteDict, tip_overlap_version: str, overlap_data: Dict[str, float], + available_sensors: AvailableSensorDefinition, ) -> None: """It should return config data given a PipetteDict.""" result = subject.get_pipette_static_config(pipette_dict, tip_overlap_version) @@ -292,6 +339,9 @@ def test_get_pipette_static_config( "t200": {"minHeight": 0.5, "minVolume": 0}, "t1000": {"minHeight": 0.5, "minVolume": 0}, }, + plunger_positions={"top": 100, "bottom": 20, "blow_out": 10, "drop_tip": 0}, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) 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 42ee037c1ce..b145458649d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -101,6 +101,14 @@ from ...protocol_runner.test_json_translator import _load_labware_definition_data +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + @pytest.fixture def mock_labware_view(decoy: Decoy) -> LabwareView: """Get a mock in the shape of a LabwareView.""" @@ -2575,6 +2583,7 @@ def test_get_next_drop_tip_location( pipette_mount: MountType, expected_locations: List[DropTipWellLocation], supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> None: """It should provide the next location to drop tips into within a labware.""" decoy.when(mock_labware_view.is_fixed_trash(labware_id="abc")).then_return(True) @@ -2611,6 +2620,14 @@ def test_get_next_drop_tip_location( back_right_corner=Point(x=40, y=20, z=60), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) ) decoy.when(mock_pipette_view.get_mount("pip-123")).then_return(pipette_mount) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 31b1a7f3a2c..e88f7886b81 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -53,6 +53,14 @@ from ..pipette_fixtures import get_default_nozzle_map +@pytest.fixture +def available_sensors() -> pipette_definition.AvailableSensorDefinition: + """Provide a list of sensors.""" + return pipette_definition.AvailableSensorDefinition( + sensors=["pressure", "capacitive", "environment"] + ) + + @pytest.fixture def subject() -> PipetteStore: """Get a PipetteStore test subject for all subsequent tests.""" @@ -190,6 +198,7 @@ def test_location_state_update(subject: PipetteStore) -> None: def test_handles_load_pipette( subject: PipetteStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> None: """It should add the pipette data to the state.""" dummy_command = create_succeeded_command() @@ -220,6 +229,14 @@ def test_handles_load_pipette( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) config_update = update_types.PipetteConfigUpdate( pipette_id="pipette-id", @@ -745,6 +762,7 @@ def test_set_movement_speed(subject: PipetteStore) -> None: def test_add_pipette_config( subject: PipetteStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: pipette_definition.AvailableSensorDefinition, ) -> None: """It should update state from any pipette config private result.""" command = cmd.LoadPipette.construct( # type: ignore[call-arg] @@ -772,6 +790,14 @@ def test_add_pipette_config( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) subject.handle_action( @@ -810,6 +836,14 @@ def test_add_pipette_config( back_right_corner=Point(x=4, y=2, z=3), ), lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) assert subject.state.flow_rates_by_id["pipette-id"].default_aspirate == {"a": 1.0} assert subject.state.flow_rates_by_id["pipette-id"].default_dispense == {"b": 2.0} diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index 64e663a24e5..c3addf9f1d7 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -9,7 +9,10 @@ from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.pipette import pipette_definition -from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps +from opentrons_shared_data.pipette.pipette_definition import ( + ValidNozzleMaps, + AvailableSensorDefinition, +) from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control import CriticalPoint @@ -58,6 +61,12 @@ ) +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + def get_pipette_view( pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None, current_well: Optional[CurrentPipetteLocation] = None, @@ -269,6 +278,7 @@ def test_get_aspirated_volume(decoy: Decoy) -> None: def test_get_pipette_working_volume( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should get the minimum value of tip volume and max volume.""" subject = get_pipette_view( @@ -291,6 +301,14 @@ def test_get_pipette_working_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) }, ) @@ -300,6 +318,7 @@ def test_get_pipette_working_volume( def test_get_pipette_working_volume_raises_if_tip_volume_is_none( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """Should raise an exception that no tip is attached.""" subject = get_pipette_view( @@ -322,6 +341,14 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) }, ) @@ -334,7 +361,9 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( def test_get_pipette_available_volume( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, decoy: Decoy + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + decoy: Decoy, + available_sensors: AvailableSensorDefinition, ) -> None: """It should get the available volume for a pipette.""" stack = decoy.mock(cls=fluid_stack.FluidStack) @@ -364,6 +393,14 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), "pipette-id-none": StaticPipetteConfig( min_volume=1, @@ -380,6 +417,14 @@ def test_get_pipette_available_volume( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), }, ) @@ -475,6 +520,7 @@ def test_get_deck_point( def test_get_static_config( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the static pipette configuration that was set for the given pipette.""" config = StaticPipetteConfig( @@ -492,6 +538,14 @@ def test_get_static_config( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) subject = get_pipette_view( @@ -523,6 +577,7 @@ def test_get_static_config( def test_get_nominal_tip_overlap( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the static pipette configuration that was set for the given pipette.""" config = StaticPipetteConfig( @@ -543,6 +598,14 @@ def test_get_nominal_tip_overlap( default_nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE), pipette_bounding_box_offsets=_SAMPLE_PIPETTE_BOUNDING_BOX_OFFSETS, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) subject = get_pipette_view(static_config_by_id={"pipette-id": config}) @@ -944,6 +1007,7 @@ def test_get_pipette_bounds_at_location( destination_position: Point, critical_point: Optional[CriticalPoint], pipette_bounds_result: Tuple[Point, Point, Point, Point], + available_sensors: AvailableSensorDefinition, ) -> None: """It should get the pipette's nozzle's bounds at the given location.""" subject = get_pipette_view( @@ -967,6 +1031,14 @@ def test_get_pipette_bounds_at_location( bounding_nozzle_offsets=_SAMPLE_NOZZLE_BOUNDS_OFFSETS, pipette_bounding_box_offsets=bounding_box_offsets, lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ) }, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index abb408d7418..7a958a37e5f 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -22,6 +22,9 @@ ) from opentrons.types import DeckSlotName, Point from opentrons_shared_data.pipette.types import PipetteNameType +from opentrons_shared_data.pipette.pipette_definition import ( + AvailableSensorDefinition, +) from ..pipette_fixtures import ( NINETY_SIX_MAP, NINETY_SIX_COLS, @@ -32,6 +35,12 @@ _tip_rack_parameters = LabwareParameters.construct(isTiprack=True) # type: ignore[call-arg] +@pytest.fixture +def available_sensors() -> AvailableSensorDefinition: + """Provide a list of sensors.""" + return AvailableSensorDefinition(sensors=["pressure", "capacitive", "environment"]) + + @pytest.fixture def subject() -> TipStore: """Get a TipStore test subject.""" @@ -94,6 +103,7 @@ def test_get_next_tip_returns_none( load_labware_action: actions.SucceedCommandAction, subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action(load_labware_action) @@ -119,6 +129,14 @@ def test_get_next_tip_returns_none( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -144,6 +162,7 @@ def test_get_next_tip_returns_first_tip( subject: TipStore, input_tip_amount: int, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action(load_labware_action) @@ -177,6 +196,14 @@ def test_get_next_tip_returns_first_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -203,6 +230,7 @@ def test_get_next_tip_used_starting_tip( input_tip_amount: int, result_well_name: str, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should start searching at the given starting tip.""" subject.handle_action(load_labware_action) @@ -229,6 +257,14 @@ def test_get_next_tip_used_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -270,6 +306,7 @@ def test_get_next_tip_skips_picked_up_tip( input_starting_tip: Optional[str], result_well_name: Optional[str], supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should get the next tip in the column if one has been picked up.""" subject.handle_action(load_labware_action) @@ -314,6 +351,14 @@ def test_get_next_tip_skips_picked_up_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -351,6 +396,7 @@ def test_get_next_tip_with_starting_tip( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action(load_labware_action) @@ -377,6 +423,14 @@ def test_get_next_tip_with_starting_tip( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -418,6 +472,7 @@ def test_get_next_tip_with_starting_tip_8_channel( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action(load_labware_action) @@ -444,6 +499,14 @@ def test_get_next_tip_with_starting_tip_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -488,6 +551,7 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the first tip of column 2 for the 8 channel after performing a single tip pickup on column 1.""" subject.handle_action(load_labware_action) @@ -514,6 +578,14 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -545,6 +617,14 @@ def test_get_next_tip_with_1_channel_followed_by_8_channel( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -589,6 +669,7 @@ def test_get_next_tip_with_starting_tip_out_of_tips( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the starting tip of H12 and then None after that.""" subject.handle_action(load_labware_action) @@ -615,6 +696,14 @@ def test_get_next_tip_with_starting_tip_out_of_tips( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -659,6 +748,7 @@ def test_get_next_tip_with_column_and_starting_tip( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should return the first tip in a column, taking starting tip into account.""" subject.handle_action(load_labware_action) @@ -685,6 +775,14 @@ def test_get_next_tip_with_column_and_starting_tip( back_left_corner_offset=Point(0, 0, 0), front_right_corner_offset=Point(0, 0, 0), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -708,6 +806,7 @@ def test_reset_tips( subject: TipStore, load_labware_action: actions.SucceedCommandAction, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """It should be able to reset tip tracking state.""" subject.handle_action(load_labware_action) @@ -734,6 +833,14 @@ def test_reset_tips( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) @@ -771,7 +878,9 @@ def get_result() -> str | None: def test_handle_pipette_config_action( - subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + available_sensors: AvailableSensorDefinition, ) -> None: """Should add pipette channel to state.""" config_update = update_types.PipetteConfigUpdate( @@ -796,6 +905,14 @@ def test_handle_pipette_config_action( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -904,6 +1021,7 @@ def test_active_channels( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, nozzle_map: NozzleMap, expected_channels: int, + available_sensors: AvailableSensorDefinition, ) -> None: """Should update active channels after pipette configuration change.""" # Load pipette to update state @@ -929,6 +1047,14 @@ def test_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -961,6 +1087,7 @@ def test_next_tip_uses_active_channels( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test that tip tracking logic uses pipette's active channels.""" # Load labware @@ -989,6 +1116,14 @@ def test_next_tip_uses_active_channels( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -1059,6 +1194,7 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test tip tracking logic using multiple pipette configurations.""" # Load labware @@ -1087,6 +1223,14 @@ def test_next_tip_automatic_tip_tracking_with_partial_configurations( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( @@ -1211,6 +1355,7 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( subject: TipStore, supported_tip_fixture: pipette_definition.SupportedTipsDefinition, load_labware_action: actions.SucceedCommandAction, + available_sensors: AvailableSensorDefinition, ) -> None: """Test tip tracking logic to ensure once a tiprack is consumed it returns None when consuming tips using multiple pipette configurations.""" # Load labware @@ -1239,6 +1384,14 @@ def test_next_tip_automatic_tip_tracking_tiprack_limits( back_left_corner_offset=Point(x=1, y=2, z=3), front_right_corner_offset=Point(x=4, y=5, z=6), pipette_lld_settings={}, + plunger_positions={ + "top": 0.0, + "bottom": 5.0, + "blow_out": 19.0, + "drop_tip": 20.0, + }, + shaft_ul_per_mm=5.0, + available_sensors=available_sensors, ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index bc581114ab2..d7e4b32e02a 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -997,8 +997,7 @@ async def test_estop_noops_if_invalid( subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(), # type: ignore - ignore_extra_args=True, + action_dispatcher.dispatch(expected_action), times=0, ) decoy.verify( diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 2f06e27c2c2..15e0192175e 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -448,6 +448,7 @@ async def test_run_json_runner_stop_requested_stops_enqueuing( await run_func() +@pytest.mark.filterwarnings("ignore::decoy.warnings.RedundantVerifyWarning") @pytest.mark.parametrize( "schema_version, json_protocol", [ diff --git a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py index ff0938a2e6d..c2cea3e0e7e 100644 --- a/api/tests/opentrons/protocol_runner/test_run_orchestrator.py +++ b/api/tests/opentrons/protocol_runner/test_run_orchestrator.py @@ -525,7 +525,7 @@ def get_next_to_execute() -> Generator[str, None, None]: index = index + 1 -async def test_create_error_recovery_policy( +def test_create_error_recovery_policy( decoy: Decoy, mock_protocol_engine: ProtocolEngine, live_protocol_subject: RunOrchestrator, diff --git a/app-shell-odd/Makefile b/app-shell-odd/Makefile index 5d2d7ac37bd..f94cab4a611 100644 --- a/app-shell-odd/Makefile +++ b/app-shell-odd/Makefile @@ -72,7 +72,7 @@ dist-ot3: clean lib NO_USB_DETECTION=true OT_APP_DEPLOY_BUCKET=opentrons-app OT_APP_DEPLOY_FOLDER=builds OPENTRONS_PROJECT=$(OPENTRONS_PROJECT) $(builder) --linux --arm64 .PHONY: push-ot3 -push-ot3: dist-ot3 deps +push-ot3: deps dist-ot3 tar -zcvf opentrons-robot-app.tar.gz -C ./dist/linux-arm64-unpacked/ ./ scp $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) -r ./opentrons-robot-app.tar.gz root@$(host): ssh $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) root@$(host) "mount -o remount,rw / && systemctl stop opentrons-robot-app && rm -rf /opt/opentrons-app && mkdir -p /opt/opentrons-app" diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index be1008ec824..2a2b46ebb7c 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,10 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.3.0-alpha.0 + +This internal release, pulled from the `edge` branch, contains features being developed for evo tip functionality. It's for internal testing only. + ## Internal Release 2.2.0-alpha.1 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. diff --git a/app/package.json b/app/package.json index e68327af200..e87da02e4b0 100644 --- a/app/package.json +++ b/app/package.json @@ -63,6 +63,7 @@ "reselect": "4.0.0", "rxjs": "^6.5.1", "semver": "5.7.2", + "simple-keyboard-layouts": "3.4.41", "styled-components": "5.3.6", "typeface-open-sans": "0.0.75", "uuid": "3.2.1" diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 5e40c7ce5e2..79416a09f73 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -177,6 +177,7 @@ "never": "Never", "new_features": "New Features", "next_step": "Next step", + "no_calibration_required": "No calibration required", "no_connection_found": "No connection found", "no_gripper_attached": "No gripper attached", "no_modules_attached": "No modules attached", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 4ff0039bbbd..2842f9dc30d 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -79,6 +79,7 @@ "tc_starting_extended_profile_cycle": "{{repetitions}} repetitions of the following steps:", "tc_starting_profile": "Running thermocycler profile with {{stepCount}} steps:", "touch_tip": "Touching tip", + "trash_bin": "Trash Bin", "trash_bin_in_slot": "Trash Bin in {{slot_name}}", "turning_rail_lights_off": "Turning rail lights off", "turning_rail_lights_on": "Turning rail lights on", diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index 1fa59e2230a..61e4f80d0ca 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -68,3 +68,12 @@ the rest is the same */ height: 44.75px; width: 330px !important; } + +.hg-candidate-box { + max-width: 400px; +} + +li.hg-candidate-box-list-item { + height: 60px; + width: 60px; +} diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index dccad085c08..4ab8dab1274 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,6 +1,12 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' -import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import { useSelector } from 'react-redux' +import { getAppLanguage } from '/app/redux/config' +import { + alphanumericKeyboardLayout, + layoutCandidates, + customDisplay, +} from '../constants' import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' @@ -19,6 +25,7 @@ export function AlphanumericKeyboard({ debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') + const appLanguage = useSelector(getAppLanguage) const onKeyPress = (button: string): void => { if (button === '{ABC}') handleShift() if (button === '{numbers}') handleNumber() @@ -47,6 +54,9 @@ export function AlphanumericKeyboard({ onKeyPress={onKeyPress} layoutName={layoutName} layout={alphanumericKeyboardLayout} + layoutCandidates={ + appLanguage != null ? layoutCandidates[appLanguage] : undefined + } display={customDisplay} mergeDisplay={true} useButtonTag={true} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css index b3ff8968da4..4fb38eb50db 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -103,3 +103,12 @@ color: #16212d; background-color: #e3e3e3; /* grey30 */ } + +.hg-candidate-box { + max-width: 400px; +} + +li.hg-candidate-box-list-item { + height: 60px; + width: 60px; +} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 663efdd9c24..eed2a0b5934 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,6 +1,12 @@ import * as React from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' -import { customDisplay, fullKeyboardLayout } from '../constants' +import { useSelector } from 'react-redux' +import { getAppLanguage } from '/app/redux/config' +import { + customDisplay, + layoutCandidates, + fullKeyboardLayout, +} from '../constants' import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' @@ -19,6 +25,7 @@ export function FullKeyboard({ debug = false, }: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') + const appLanguage = useSelector(getAppLanguage) const handleShift = (button: string): void => { switch (button) { case '{shift}': @@ -56,6 +63,9 @@ export function FullKeyboard({ onKeyPress={onKeyPress} layoutName={layoutName} layout={fullKeyboardLayout} + layoutCandidates={ + appLanguage != null ? layoutCandidates[appLanguage] : undefined + } display={customDisplay} mergeDisplay={true} useButtonTag={true} diff --git a/app/src/atoms/SoftwareKeyboard/constants.ts b/app/src/atoms/SoftwareKeyboard/constants.ts index 1808f4bd2f3..6fccfd21b81 100644 --- a/app/src/atoms/SoftwareKeyboard/constants.ts +++ b/app/src/atoms/SoftwareKeyboard/constants.ts @@ -1,3 +1,11 @@ +import chineseLayout from 'simple-keyboard-layouts/build/layouts/chinese' + +type LayoutCandidates = + | { + [key: string]: string + } + | undefined + export const customDisplay = { '{numbers}': '123', '{shift}': 'ABC', @@ -69,3 +77,12 @@ export const numericalKeyboardLayout = { export const numericalCustom = { '{backspace}': 'del', } + +export const layoutCandidates: { + [key: string]: LayoutCandidates +} = { + // @ts-expect-error layout candidates exists but is not on the type + // in the simple-keyboard-layouts package + 'zh-CN': chineseLayout.layoutCandidates, + 'en-US': undefined, +} diff --git a/app/src/local-resources/commands/utils/index.ts b/app/src/local-resources/commands/utils/index.ts index 7aa84d14de5..cc4e9c2579a 100644 --- a/app/src/local-resources/commands/utils/index.ts +++ b/app/src/local-resources/commands/utils/index.ts @@ -1 +1,2 @@ export * from './getCommandTextData' +export * from './lastRunCommandPromptedErrorRecovery' diff --git a/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts b/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts new file mode 100644 index 00000000000..dd07756ef43 --- /dev/null +++ b/app/src/local-resources/commands/utils/lastRunCommandPromptedErrorRecovery.ts @@ -0,0 +1,13 @@ +import type { RunCommandSummary } from '@opentrons/api-client' + +// Whether the last run protocol command prompted Error Recovery. +export function lastRunCommandPromptedErrorRecovery( + summary: RunCommandSummary[] +): boolean { + const lastProtocolCommand = summary.findLast( + command => command.intent !== 'fixit' && command.error != null + ) + + // All recoverable protocol commands have defined errors. + return lastProtocolCommand?.error?.isDefined ?? false +} diff --git a/app/src/local-resources/labware/utils/__tests__/getLabwareDisplayLocation.test.tsx b/app/src/local-resources/labware/utils/__tests__/getLabwareDisplayLocation.test.tsx index ca4b095f00e..beed2d012c0 100644 --- a/app/src/local-resources/labware/utils/__tests__/getLabwareDisplayLocation.test.tsx +++ b/app/src/local-resources/labware/utils/__tests__/getLabwareDisplayLocation.test.tsx @@ -137,6 +137,24 @@ describe('getLabwareDisplayLocation with translations', () => { screen.getByText('Slot C1') }) + it('should special case the slotName if it contains "waste chute"', () => { + render({ + location: { slotName: 'gripperWasteChute' }, + params: { ...defaultParams, detailLevel: 'slot-only' }, + }) + + screen.getByText('Waste Chute') + }) + + it('should special case the slotName if it contains "trash bin"', () => { + render({ + location: { slotName: 'trashBin' }, + params: { ...defaultParams, detailLevel: 'slot-only' }, + }) + + screen.getByText('Trash Bin') + }) + it('should handle an adapter on module location when the detail level is full', () => { const mockLoadedLabwares = [ { diff --git a/app/src/local-resources/labware/utils/getLabwareDisplayLocation.ts b/app/src/local-resources/labware/utils/getLabwareDisplayLocation.ts index 2e02199e667..63804cba764 100644 --- a/app/src/local-resources/labware/utils/getLabwareDisplayLocation.ts +++ b/app/src/local-resources/labware/utils/getLabwareDisplayLocation.ts @@ -4,6 +4,8 @@ import { getOccludedSlotCountForModule, THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, + TRASH_BIN_FIXTURE, + WASTE_CHUTE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { getLabwareLocation } from './getLabwareLocation' @@ -12,6 +14,7 @@ import type { LocationSlotOnlyParams, LocationFullParams, } from './getLabwareLocation' +import type { AddressableAreaName } from '@opentrons/shared-data' export interface DisplayLocationSlotOnlyParams extends LocationSlotOnlyParams { t: TFunction @@ -47,7 +50,8 @@ export function getLabwareDisplayLocation( } // Simple slot location else if (moduleModel == null && adapterName == null) { - return isOnDevice ? slotName : t('slot', { slot_name: slotName }) + const validatedSlotCopy = handleSpecialSlotNames(slotName, t) + return isOnDevice ? validatedSlotCopy.odd : validatedSlotCopy.desktop } // Module location without adapter else if (moduleModel != null && adapterName == null) { @@ -91,3 +95,20 @@ export function getLabwareDisplayLocation( return '' } } + +// Sometimes we don't want to show the actual slotName, so we special case the text here. +function handleSpecialSlotNames( + slotName: string, + t: TFunction +): { odd: string; desktop: string } { + if (WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotName as AddressableAreaName)) { + return { odd: t('waste_chute'), desktop: t('waste_chute') } + } else if (slotName === TRASH_BIN_FIXTURE) { + return { odd: t('trash_bin'), desktop: t('trash_bin') } + } else { + return { + odd: slotName, + desktop: t('slot', { slot_name: slotName }), + } + } +} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts index e41b7edc8ec..687b0d404c5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts @@ -1,23 +1,23 @@ import { useEffect } from 'react' -import { useHost } from '@opentrons/react-api-client' import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' -import { - useDropTipWizardFlows, - useTipAttachmentStatus, -} from '/app/organisms/DropTipWizardFlows' +import { useDropTipWizardFlows } from '/app/organisms/DropTipWizardFlows' import { useProtocolDropTipModal } from '../modals' -import { useCloseCurrentRun, useIsRunCurrent } from '/app/resources/runs' +import { + useCloseCurrentRun, + useCurrentRunCommands, + useIsRunCurrent, +} from '/app/resources/runs' import { isTerminalRunStatus } from '../../utils' +import { lastRunCommandPromptedErrorRecovery } from '/app/local-resources/commands' +import { useTipAttachmentStatus } from '/app/resources/instruments' import type { RobotType } from '@opentrons/shared-data' import type { Run, RunStatus } from '@opentrons/api-client' -import type { - DropTipWizardFlowsProps, - PipetteWithTip, -} from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' +import type { DropTipWizardFlowsProps } from '/app/organisms/DropTipWizardFlows' import type { UseProtocolDropTipModalResult } from '../modals' import type { PipetteDetails } from '/app/resources/maintenance_runs' @@ -44,7 +44,6 @@ export function useRunHeaderDropTip({ robotType, runStatus, }: UseRunHeaderDropTipParams): UseRunHeaderDropTipResult { - const host = useHost() const isRunCurrent = useIsRunCurrent(runId) const enteredER = runRecord?.data.hasEverEnteredErrorRecovery ?? false @@ -61,7 +60,6 @@ export function useRunHeaderDropTip({ } = useTipAttachmentStatus({ runId, runRecord: runRecord ?? null, - host, }) const dropTipModalUtils = useProtocolDropTipModal({ @@ -102,6 +100,15 @@ export function useRunHeaderDropTip({ : { showDTWiz: false, dtWizProps: null } } + const runSummaryNoFixit = useCurrentRunCommands( + { + includeFixitCommands: false, + pageLength: 1, + cursor: null, + }, + { enabled: isTerminalRunStatus(runStatus) } + ) + // Manage tip checking useEffect(() => { // If a user begins a new run without navigating away from the run page, reset tip status. @@ -111,11 +118,14 @@ export function useRunHeaderDropTip({ } // Only determine tip status when necessary as this can be an expensive operation. Error Recovery handles tips, so don't // have to do it here if done during Error Recovery. - else if (isTerminalRunStatus(runStatus) && !enteredER) { + else if ( + runSummaryNoFixit != null && + !lastRunCommandPromptedErrorRecovery(runSummaryNoFixit) + ) { void determineTipStatus() } } - }, [runStatus, robotType, enteredER]) + }, [runStatus, robotType, runSummaryNoFixit]) // TODO(jh, 08-15-24): The enteredER condition is a hack, because errorCommands are only returned when a run is current. // Ideally the run should not need to be current to view errorCommands. diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx index e1f1be57d22..b9f30de446f 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx @@ -21,7 +21,7 @@ import { useHomePipettes } from '/app/local-resources/instruments' import type { PipetteData } from '@opentrons/api-client' import type { IconProps } from '@opentrons/components' import type { UseHomePipettesProps } from '/app/local-resources/instruments' -import type { TipAttachmentStatusResult } from '/app/organisms/DropTipWizardFlows' +import type { TipAttachmentStatusResult } from '/app/resources/instruments' type UseProtocolDropTipModalProps = Pick< UseHomePipettesProps, diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx index 9aa6b7cee22..6acaf42445b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx @@ -3,6 +3,8 @@ import { fireEvent, screen } from '@testing-library/react' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' import { when } from 'vitest-when' +import { useHoverTooltip } from '@opentrons/components' + import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { useLPCSuccessToast } from '../../../hooks/useLPCSuccessToast' @@ -20,6 +22,13 @@ import { useUnmatchedModulesForProtocol, } from '/app/resources/runs' +vi.mock('@opentrons/components', async () => { + const actual = await vi.importActual('@opentrons/components') + return { + ...actual, + useHoverTooltip: vi.fn(), + } +}) vi.mock('../SetupLabwareList') vi.mock('../SetupLabwareMap') vi.mock('/app/organisms/LabwarePositionCheck') @@ -78,7 +87,6 @@ describe('SetupLabware', () => { .thenReturn({ complete: true, }) - when(vi.mocked(useRunHasStarted)).calledWith(RUN_ID).thenReturn(false) vi.mocked(getIsLabwareOffsetCodeSnippetsOn).mockReturnValue(false) vi.mocked(SetupLabwareMap).mockReturnValue(
mock setup labware map
@@ -88,6 +96,8 @@ describe('SetupLabware', () => { ) vi.mocked(useLPCDisabledReason).mockReturnValue(null) vi.mocked(useNotifyRunQuery).mockReturnValue({} as any) + vi.mocked(useHoverTooltip).mockReturnValue([{}, {}] as any) + vi.mocked(useRunHasStarted).mockReturnValue(false) }) afterEach(() => { @@ -98,8 +108,21 @@ describe('SetupLabware', () => { render() screen.getByText('mock setup labware list') screen.getByRole('button', { name: 'List View' }) + screen.getByRole('button', { name: 'Confirm placements' }) const mapView = screen.getByRole('button', { name: 'Map View' }) fireEvent.click(mapView) screen.getByText('mock setup labware map') }) + + it('disables the confirmation button if the run has already started', () => { + vi.mocked(useRunHasStarted).mockReturnValue(true) + + render() + + const btn = screen.getByRole('button', { + name: 'Confirm placements', + }) + + expect(btn).toBeDisabled() + }) }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/index.tsx index 38963d79dda..687c1a739ab 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabware/index.tsx @@ -1,17 +1,22 @@ import { useTranslation } from 'react-i18next' import map from 'lodash/map' + import { JUSTIFY_CENTER, Flex, SPACING, PrimaryButton, DIRECTION_COLUMN, + Tooltip, + useHoverTooltip, } from '@opentrons/components' + import { useToggleGroup } from '/app/molecules/ToggleGroup/useToggleGroup' import { getModuleTypesThatRequireExtraAttention } from '../utils/getModuleTypesThatRequireExtraAttention' import { useMostRecentCompletedAnalysis, useModuleRenderInfoForProtocolById, + useRunHasStarted, } from '/app/resources/runs' import { useIsFlex } from '/app/redux-resources/robots' import { useStoredProtocolAnalysis } from '/app/resources/analysis' @@ -46,6 +51,11 @@ export function SetupLabware(props: SetupLabwareProps): JSX.Element { moduleModels ) + // TODO(jh, 11-13-24): These disabled tooltips are used throughout setup flows. Let's consolidate them. + const [targetProps, tooltipProps] = useHoverTooltip() + const runHasStarted = useRunHasStarted(runId) + const tooltipText = runHasStarted ? t('protocol_run_started') : null + return ( <> { setLabwareConfirmed(true) }} - disabled={labwareConfirmed} + disabled={labwareConfirmed || runHasStarted} + {...targetProps} > {t('confirm_placements')} + {tooltipText != null ? ( + {tooltipText} + ) : null} ) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 9d2c6223373..637a4814936 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -143,7 +143,6 @@ export function SetupLabwarePositionCheck( ) : null} { launchLPC() setIsShowingLPCSuccessToast(false) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx index 097f30447ee..736d5f5bc85 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquids.test.tsx @@ -1,15 +1,26 @@ import type * as React from 'react' -import { describe, it, beforeEach, vi } from 'vitest' +import { describe, it, beforeEach, vi, expect } from 'vitest' import { screen, fireEvent } from '@testing-library/react' +import { useHoverTooltip } from '@opentrons/components' + import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { SetupLiquids } from '../index' import { SetupLiquidsList } from '../SetupLiquidsList' import { SetupLiquidsMap } from '../SetupLiquidsMap' +import { useRunHasStarted } from '/app/resources/runs' +vi.mock('@opentrons/components', async () => { + const actual = await vi.importActual('@opentrons/components') + return { + ...actual, + useHoverTooltip: vi.fn(), + } +}) vi.mock('../SetupLiquidsList') vi.mock('../SetupLiquidsMap') +vi.mock('/app/resources/runs') describe('SetupLiquids', () => { const render = ( @@ -44,6 +55,8 @@ describe('SetupLiquids', () => { vi.mocked(SetupLiquidsMap).mockReturnValue(
Mock setup liquids map
) + vi.mocked(useHoverTooltip).mockReturnValue([{}, {}] as any) + vi.mocked(useRunHasStarted).mockReturnValue(false) }) it('renders the list and map view buttons and proceed button', () => { @@ -64,4 +77,15 @@ describe('SetupLiquids', () => { fireEvent.click(mapViewButton) screen.getByText('Mock setup liquids list') }) + it('disables the confirmation button if the run has already started', () => { + vi.mocked(useRunHasStarted).mockReturnValue(true) + + render(props) + + const btn = screen.getByRole('button', { + name: 'Confirm locations and volumes', + }) + + expect(btn).toBeDisabled() + }) }) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/index.tsx index 28a6f84e2d4..685d14a2ae5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLiquids/index.tsx @@ -6,11 +6,14 @@ import { DIRECTION_COLUMN, ALIGN_CENTER, PrimaryButton, + useHoverTooltip, + Tooltip, } from '@opentrons/components' import { useToggleGroup } from '/app/molecules/ToggleGroup/useToggleGroup' import { ANALYTICS_LIQUID_SETUP_VIEW_TOGGLE } from '/app/redux/analytics' import { SetupLiquidsList } from './SetupLiquidsList' import { SetupLiquidsMap } from './SetupLiquidsMap' +import { useRunHasStarted } from '/app/resources/runs' import type { CompletedProtocolAnalysis, @@ -38,6 +41,12 @@ export function SetupLiquids({ t('map_view') as string, ANALYTICS_LIQUID_SETUP_VIEW_TOGGLE ) + + // TODO(jh, 11-13-24): These disabled tooltips are used throughout setup flows. Let's consolidate them. + const [targetProps, tooltipProps] = useHoverTooltip() + const runHasStarted = useRunHasStarted(runId) + const tooltipText = runHasStarted ? t('protocol_run_started') : null + return ( { setLiquidSetupConfirmed(true) }} - disabled={isLiquidSetupConfirmed} + disabled={isLiquidSetupConfirmed || runHasStarted} + {...targetProps} > {t('confirm_locations_and_volumes')}
+ {tooltipText != null ? ( + {tooltipText} + ) : null} ) diff --git a/app/src/organisms/Desktop/ProtocolDetails/index.tsx b/app/src/organisms/Desktop/ProtocolDetails/index.tsx index a54115a00f9..3bdf4be672e 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/index.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/index.tsx @@ -47,7 +47,6 @@ import { parseInitialLoadedLabwareBySlot, parseInitialLoadedModulesBySlot, parseInitialPipetteNamesByMount, - NON_USER_ADDRESSABLE_LABWARE, } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' @@ -285,9 +284,7 @@ export function ProtocolDetails( : [] ), }).filter( - labware => - labware.result?.definition?.parameters?.format !== 'trash' && - !NON_USER_ADDRESSABLE_LABWARE.includes(labware?.params?.loadName) + labware => labware.result?.definition?.parameters?.format !== 'trash' ) : [] diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationItems.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationItems.tsx index 11ee8f60402..684b8269784 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationItems.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationItems.tsx @@ -8,7 +8,10 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { getModuleDisplayName } from '@opentrons/shared-data' +import { + getModuleDisplayName, + ABSORBANCE_READER_TYPE, +} from '@opentrons/shared-data' import { formatLastCalibrated } from './utils' import { ModuleCalibrationOverflowMenu } from './ModuleCalibrationOverflowMenu' @@ -41,42 +44,51 @@ export function ModuleCalibrationItems({ - {attachedModules.map(attachedModule => ( - - - - {getModuleDisplayName(attachedModule.moduleModel)} - - - - - {attachedModule.serialNumber} - - - - - {attachedModule.moduleOffset?.last_modified != null - ? formatLastCalibrated( - attachedModule.moduleOffset?.last_modified - ) - : t('not_calibrated_short')} - - - - - - - ))} + {attachedModules.map(attachedModule => { + const noCalibrationCopy = + attachedModule.moduleType === ABSORBANCE_READER_TYPE + ? t('no_calibration_required') + : t('not_calibrated_short') + + return ( + + + + {getModuleDisplayName(attachedModule.moduleModel)} + + + + + {attachedModule.serialNumber} + + + + + {attachedModule.moduleOffset?.last_modified != null + ? formatLastCalibrated( + attachedModule.moduleOffset?.last_modified + ) + : noCalibrationCopy} + + + + {attachedModule.moduleType !== ABSORBANCE_READER_TYPE ? ( + + ) : null} + + + ) + })} ) diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx index 2fdd9694e5d..8cb0dd62dc6 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationItems.test.tsx @@ -9,6 +9,7 @@ import { formatLastCalibrated } from '../utils' import { ModuleCalibrationItems } from '../ModuleCalibrationItems' import type { AttachedModule } from '@opentrons/api-client' +import { ABSORBANCE_READER_TYPE } from '@opentrons/shared-data' vi.mock('../ModuleCalibrationOverflowMenu') @@ -42,7 +43,7 @@ const mockCalibratedModule = { totalCycleCount: 1, currentStepIndex: 1, totalStepCount: 1, - }, + } as any, usbPort: { port: 3, portGroup: 'left', @@ -101,4 +102,23 @@ describe('ModuleCalibrationItems', () => { render(props) screen.getByText(formatLastCalibrated('2023-06-01T14:42:20.131798+00:00')) }) + + it('should say no calibration required if module is absorbance reader', () => { + const absorbanceReaderAttachedModule = { + ...mockCalibratedModule, + moduleType: ABSORBANCE_READER_TYPE, + moduleOffset: undefined, + } + props = { + ...props, + attachedModules: [ + absorbanceReaderAttachedModule as AttachedModule, + ] as AttachedModule[], + } + render(props) + expect( + screen.queryByText('mock ModuleCalibrationOverflowMenu') + ).not.toBeInTheDocument() + screen.getByText('No calibration required') + }) }) diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx index a91d7389072..8ed93c1cb81 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/__tests__/SystemLanguagePreferenceModal.test.tsx @@ -180,4 +180,16 @@ describe('SystemLanguagePreferenceModal', () => { 'zh-Hant' ) }) + + it('should not open update modal when system language changes to an unsuppported language', () => { + vi.mocked(getSystemLanguage).mockReturnValue('es-MX') + render() + + expect(screen.queryByRole('button', { name: 'Don’t change' })).toBeNull() + expect( + screen.queryByRole('button', { + name: 'Use system language', + }) + ).toBeNull() + }) }) diff --git a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx index 1a3a0d7d9ba..b4bf54c0d17 100644 --- a/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx +++ b/app/src/organisms/Desktop/SystemLanguagePreferenceModal/index.tsx @@ -46,11 +46,7 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { const storedSystemLanguage = useSelector(getStoredSystemLanguage) const showBootModal = appLanguage == null && systemLanguage != null - const showUpdateModal = - appLanguage != null && - systemLanguage != null && - storedSystemLanguage != null && - systemLanguage !== storedSystemLanguage + const [showUpdateModal, setShowUpdateModal] = useState(false) const title = showUpdateModal ? t('system_language_preferences_update') @@ -120,6 +116,13 @@ export function SystemLanguagePreferenceModal(): JSX.Element | null { void i18n.changeLanguage(systemLanguage) } } + // only show update modal if we support the language their system has updated to + setShowUpdateModal( + appLanguage != null && + matchedSystemLanguageOption != null && + storedSystemLanguage != null && + systemLanguage !== storedSystemLanguage + ) } }, [i18n, systemLanguage, showBootModal]) diff --git a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx index be4cc9979ef..228d4f3384c 100644 --- a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx +++ b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx @@ -246,7 +246,13 @@ export const DropTipWizardContent = ( function buildModalContent(): JSX.Element { // Don't render the spinner screen for 1 render cycle on fixit commands. - if (currentStep === BEFORE_BEGINNING && issuedCommandsType === 'fixit') { + + if (errorDetails != null) { + return buildErrorScreen() + } else if ( + currentStep === BEFORE_BEGINNING && + issuedCommandsType === 'fixit' + ) { return buildBeforeBeginning() } else if ( activeMaintenanceRunId == null && @@ -259,8 +265,6 @@ export const DropTipWizardContent = ( return buildRobotInMotion() } else if (showConfirmExit) { return buildShowExitConfirmation() - } else if (errorDetails != null) { - return buildErrorScreen() } else if (currentStep === BEFORE_BEGINNING) { return buildBeforeBeginning() } else if (currentStep === CHOOSE_LOCATION_OPTION) { diff --git a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx index 7198d8bb5ea..86778afe97b 100644 --- a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx +++ b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx @@ -19,8 +19,8 @@ import { useHomePipettes } from '/app/local-resources/instruments' import type { HostConfig } from '@opentrons/api-client' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' import type { UseHomePipettesProps } from '/app/local-resources/instruments' -import type { PipetteWithTip } from './hooks' import type { PipetteDetails } from '/app/resources/maintenance_runs' +import type { PipetteWithTip } from '/app/resources/instruments' type TipsAttachedModalProps = Pick & { aPipetteWithTip: PipetteWithTip diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx index 917c770c10e..2a71920c4fc 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/TipsAttachedModal.test.tsx @@ -14,7 +14,7 @@ import { useDropTipWizardFlows } from '..' import type { Mock } from 'vitest' import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { HostConfig } from '@opentrons/api-client' -import type { PipetteWithTip } from '../hooks' +import type { PipetteWithTip } from '/app/resources/instruments' vi.mock('/app/resources/runs/useCloseCurrentRun') vi.mock('..') diff --git a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx index c6cf823784c..f5622960244 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useDropTipRouting.test.tsx @@ -144,4 +144,15 @@ describe('getInitialRouteAndStep', () => { expect(initialRoute).toBe(DT_ROUTES.DROP_TIP) expect(initialStep).toBe(DT_ROUTES.DROP_TIP[2]) }) + + it('should return the overridden route and first step when fixitUtils.routeOverride.route is provided but routeOverride.step is not provided', () => { + const fixitUtils = { + routeOverride: { route: DT_ROUTES.DROP_TIP, step: null }, + } as any + + const [initialRoute, initialStep] = getInitialRouteAndStep(fixitUtils) + + expect(initialRoute).toBe(DT_ROUTES.DROP_TIP) + expect(initialStep).toBe(DT_ROUTES.DROP_TIP[0]) + }) }) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx b/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx deleted file mode 100644 index 6d9d25719d2..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/__tests__/useTipAttachmentStatus.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { act, renderHook } from '@testing-library/react' - -import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { useInstrumentsQuery } from '@opentrons/react-api-client' - -import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' -import { getPipettesWithTipAttached } from '../useTipAttachmentStatus/getPipettesWithTipAttached' -import { DropTipWizard } from '../../DropTipWizard' -import { useTipAttachmentStatus } from '../useTipAttachmentStatus' - -import type { Mock } from 'vitest' -import type { PipetteModelSpecs } from '@opentrons/shared-data' -import type { PipetteWithTip } from '../useTipAttachmentStatus' - -vi.mock('@opentrons/shared-data', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - getPipetteModelSpecs: vi.fn(), - } -}) -vi.mock('@opentrons/react-api-client') -vi.mock('../useTipAttachmentStatus/getPipettesWithTipAttached') -vi.mock('../../DropTipWizard') - -const MOCK_ACTUAL_PIPETTE = { - ...mockPipetteInfo.pipetteSpecs, - model: 'model', - tipLength: { - value: 20, - }, -} as PipetteModelSpecs - -const mockPipetteWithTip: PipetteWithTip = { - mount: 'left', - specs: MOCK_ACTUAL_PIPETTE, -} - -const mockSecondPipetteWithTip: PipetteWithTip = { - mount: 'right', - specs: MOCK_ACTUAL_PIPETTE, -} - -const mockPipettesWithTip: PipetteWithTip[] = [ - mockPipetteWithTip, - mockSecondPipetteWithTip, -] - -describe('useTipAttachmentStatus', () => { - let mockGetPipettesWithTipAttached: Mock - - beforeEach(() => { - mockGetPipettesWithTipAttached = vi.mocked(getPipettesWithTipAttached) - vi.mocked(getPipetteModelSpecs).mockReturnValue(MOCK_ACTUAL_PIPETTE) - vi.mocked(DropTipWizard).mockReturnValue(
MOCK DROP TIP WIZ
) - mockGetPipettesWithTipAttached.mockResolvedValue(mockPipettesWithTip) - vi.mocked(useInstrumentsQuery).mockReturnValue({ data: {} } as any) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - it('should return the correct initial state', () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - expect(result.current.areTipsAttached).toBe(false) - expect(result.current.aPipetteWithTip).toEqual(null) - }) - - it('should determine tip status and update state accordingly', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - }) - - expect(result.current.areTipsAttached).toBe(true) - expect(result.current.aPipetteWithTip).toEqual(mockPipetteWithTip) - }) - - it('should reset tip status', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.resetTipStatus() - }) - - expect(result.current.areTipsAttached).toBe(false) - expect(result.current.aPipetteWithTip).toEqual(null) - }) - - it('should set tip status resolved and update state', async () => { - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.setTipStatusResolved() - }) - - expect(result.current.aPipetteWithTip).toEqual(mockSecondPipetteWithTip) - }) - - it('should call onEmptyCache callback when cache becomes empty', async () => { - mockGetPipettesWithTipAttached.mockResolvedValueOnce([mockPipetteWithTip]) - - const onEmptyCacheMock = vi.fn() - const { result } = renderHook(() => useTipAttachmentStatus({} as any)) - - await act(async () => { - await result.current.determineTipStatus() - result.current.setTipStatusResolved(onEmptyCacheMock) - }) - - expect(onEmptyCacheMock).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/index.ts b/app/src/organisms/DropTipWizardFlows/hooks/index.ts index 3f3f531a9d8..f3145d7d083 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/index.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/index.ts @@ -1,6 +1,5 @@ export * from './errors' export * from './useDropTipWithType' -export * from './useTipAttachmentStatus' export * from './useDropTipLocations' export { useDropTipRouting } from './useDropTipRouting' export { useDropTipWithType } from './useDropTipWithType' diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts index 6bb8915505e..493b4a77bff 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipCommands.ts @@ -369,12 +369,6 @@ const buildBlowoutCommands = ( ), }, }, - { - commandType: 'prepareToAspirate', - params: { - pipetteId: pipetteId ?? MANAGED_PIPETTE_ID, - }, - }, Z_HOME, ] : [ diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx index b0928eca3c2..4c3e8e01064 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipRouting.tsx @@ -218,7 +218,10 @@ export function getInitialRouteAndStep( ): [DropTipFlowsRoute, DropTipFlowsStep] { const routeOverride = fixitUtils?.routeOverride const initialRoute = routeOverride?.route ?? DT_ROUTES.BEFORE_BEGINNING - const initialStep = routeOverride?.step ?? BEFORE_BEGINNING_STEPS[0] + const initialStep = + routeOverride?.step ?? + routeOverride?.route?.[0] ?? + BEFORE_BEGINNING_STEPS[0] return [initialRoute, initialStep] } diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts deleted file mode 100644 index eb969f46820..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/__tests__/getPipettesWithTipAttached.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, beforeEach, expect, vi } from 'vitest' -import { getCommands } from '@opentrons/api-client' - -import { getPipettesWithTipAttached } from '../getPipettesWithTipAttached' -import { LEFT, RIGHT } from '@opentrons/shared-data' - -import type { GetPipettesWithTipAttached } from '../getPipettesWithTipAttached' - -vi.mock('@opentrons/api-client') - -const HOST_NAME = 'localhost' -const RUN_ID = 'testRunId' -const LEFT_PIPETTE_ID = 'testId1' -const RIGHT_PIPETTE_ID = 'testId2' -const LEFT_PIPETTE_NAME = 'testLeftName' -const RIGHT_PIPETTE_NAME = 'testRightName' -const PICK_UP_TIP = 'pickUpTip' -const DROP_TIP = 'dropTip' -const DROP_TIP_IN_PLACE = 'dropTipInPlace' -const LOAD_PIPETTE = 'loadPipette' -const FIXIT_INTENT = 'fixit' - -const mockAttachedInstruments = { - data: [ - { mount: LEFT, state: { tipDetected: true } }, - { mount: RIGHT, state: { tipDetected: true } }, - ], - meta: { cursor: 0, totalLength: 2 }, -} - -const createMockCommand = ( - type: string, - id: string, - pipetteId: string, - status = 'succeeded' -) => ({ - id, - key: `${id}-key`, - commandType: type, - status, - params: { pipetteId }, -}) - -const mockCommands = { - data: [ - createMockCommand(LOAD_PIPETTE, 'load-left', LEFT_PIPETTE_ID), - createMockCommand(LOAD_PIPETTE, 'load-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left', LEFT_PIPETTE_ID), - createMockCommand(DROP_TIP, 'drop-left', LEFT_PIPETTE_ID, 'succeeded'), - ], - meta: { cursor: 0, totalLength: 4 }, -} - -const mockRunRecord = { - data: { - pipettes: [ - { id: LEFT_PIPETTE_ID, pipetteName: LEFT_PIPETTE_NAME, mount: LEFT }, - { id: RIGHT_PIPETTE_ID, pipetteName: RIGHT_PIPETTE_NAME, mount: RIGHT }, - ], - }, -} - -describe('getPipettesWithTipAttached', () => { - let DEFAULT_PARAMS: GetPipettesWithTipAttached - - beforeEach(() => { - DEFAULT_PARAMS = { - host: { hostname: HOST_NAME }, - runId: RUN_ID, - attachedInstruments: mockAttachedInstruments as any, - runRecord: mockRunRecord as any, - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommands, - } as any) - }) - - it('returns an empty array if attachedInstruments is null', async () => { - const params = { ...DEFAULT_PARAMS, attachedInstruments: null } - const result = await getPipettesWithTipAttached(params) - expect(result).toEqual([]) - }) - - it('returns an empty array if runRecord is null', async () => { - const params = { ...DEFAULT_PARAMS, runRecord: null } - const result = await getPipettesWithTipAttached(params) - expect(result).toEqual([]) - }) - - it('returns an empty array when no tips are attached according to protocol', async () => { - const mockCommandsWithoutAttachedTips = { - ...mockCommands, - data: [ - createMockCommand(LOAD_PIPETTE, 'load-left', LEFT_PIPETTE_ID), - createMockCommand(LOAD_PIPETTE, 'load-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left', LEFT_PIPETTE_ID), - createMockCommand(DROP_TIP, 'drop-left', LEFT_PIPETTE_ID, 'succeeded'), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithoutAttachedTips, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([]) - }) - - it('returns pipettes with protocol detected tip attachment', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-right', RIGHT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual(mockAttachedInstruments.data) - }) - - it('always returns the left mount before the right mount if both pipettes have tips attached', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-right', RIGHT_PIPETTE_ID), - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result.length).toBe(2) - expect(result[0].mount).toEqual(LEFT) - expect(result[1].mount).toEqual(RIGHT) - }) - - it('does not return otherwise legitimate failed tip exchange commands if fixit intent tip commands are present and successful', async () => { - const mockCommandsWithFixit = { - ...mockCommands, - data: [ - ...mockCommands.data, - { - ...createMockCommand( - DROP_TIP_IN_PLACE, - 'fixit-drop', - LEFT_PIPETTE_ID - ), - intent: FIXIT_INTENT, - }, - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithFixit, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([]) - }) - - it('considers a tip attached only if the last tip exchange command was pickUpTip', async () => { - const mockCommandsWithPickUpTip = { - ...mockCommands, - data: [ - ...mockCommands.data, - createMockCommand(PICK_UP_TIP, 'pickup-left-2', LEFT_PIPETTE_ID), - ], - } - - vi.mocked(getCommands).mockResolvedValue({ - data: mockCommandsWithPickUpTip, - } as any) - - const result = await getPipettesWithTipAttached(DEFAULT_PARAMS) - expect(result).toEqual([mockAttachedInstruments.data[0]]) - }) -}) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts deleted file mode 100644 index 42b006ca0b2..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/getPipettesWithTipAttached.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { getCommands } from '@opentrons/api-client' -import { LEFT } from '@opentrons/shared-data' - -import type { - HostConfig, - PipetteData, - Run, - CommandsData, - RunCommandSummary, - Instruments, -} from '@opentrons/api-client' -import type { - LoadedPipette, - PipettingRunTimeCommand, -} from '@opentrons/shared-data' - -export interface GetPipettesWithTipAttached { - host: HostConfig | null - runId: string - attachedInstruments: Instruments | null - runRecord: Run | null -} - -export function getPipettesWithTipAttached({ - host, - runId, - attachedInstruments, - runRecord, -}: GetPipettesWithTipAttached): Promise { - if (attachedInstruments == null || runRecord == null) { - return Promise.resolve([]) - } - - return getCommandsExecutedDuringRun( - host as HostConfig, - runId - ).then(executedCmdData => - checkPipettesForAttachedTips( - executedCmdData.data, - runRecord.data.pipettes, - attachedInstruments.data as PipetteData[] - ) - ) -} - -function getCommandsExecutedDuringRun( - host: HostConfig, - runId: string -): Promise { - return getCommands(host, runId, { - cursor: null, - pageLength: 0, - includeFixitCommands: true, - }).then(response => { - const { totalLength } = response.data.meta - return getCommands(host, runId, { - cursor: 0, - pageLength: totalLength, - includeFixitCommands: null, - }).then(response => response.data) - }) -} - -const TIP_EXCHANGE_COMMAND_TYPES = ['dropTip', 'dropTipInPlace', 'pickUpTip'] - -function checkPipettesForAttachedTips( - commands: RunCommandSummary[], - pipettesUsedInRun: LoadedPipette[], - attachedPipettes: PipetteData[] -): PipetteData[] { - let pipettesWithUnknownTipStatus = pipettesUsedInRun - const mountsWithTipAttached: Array = [] - - // Iterate backwards through commands, finding first tip exchange command for each pipette. - // If there's a chance the tip is still attached, flag the pipette. - for (let i = commands.length - 1; i >= 0; i--) { - if (pipettesWithUnknownTipStatus.length === 0) { - break - } - - const commandType = commands[i].commandType - const pipetteUsedInCommand = (commands[i] as PipettingRunTimeCommand).params - .pipetteId - const isTipExchangeCommand = TIP_EXCHANGE_COMMAND_TYPES.includes( - commandType - ) - const pipetteUsedInCommandWithUnknownTipStatus = - pipettesWithUnknownTipStatus.find( - pipette => pipette.id === pipetteUsedInCommand - ) ?? null - - // If the currently iterated command is a fixit command, we can safely assume the user - // had the option to fix pipettes with tips in this command and all commands - // earlier in the run, during Error Recovery flows. - if ( - commands[i].intent === 'fixit' && - isTipExchangeCommand && - commands[i].status === 'succeeded' - ) { - break - } - - if ( - isTipExchangeCommand && - pipetteUsedInCommandWithUnknownTipStatus != null - ) { - const tipPossiblyAttached = - commands[i].status !== 'succeeded' || commandType === 'pickUpTip' - - if (tipPossiblyAttached) { - mountsWithTipAttached.push( - pipetteUsedInCommandWithUnknownTipStatus.mount - ) - } - pipettesWithUnknownTipStatus = pipettesWithUnknownTipStatus.filter( - pipette => pipette.id !== pipetteUsedInCommand - ) - } - } - - // Convert the array of mounts with attached tips to PipetteData with attached tips. - const pipettesWithTipAttached = attachedPipettes.filter(attachedPipette => - mountsWithTipAttached.includes(attachedPipette.mount) - ) - - // Preferentially assign the left mount as the first element. - if ( - pipettesWithTipAttached.length === 2 && - pipettesWithTipAttached[1].mount === LEFT - ) { - ;[pipettesWithTipAttached[0], pipettesWithTipAttached[1]] = [ - pipettesWithTipAttached[1], - pipettesWithTipAttached[0], - ] - } - - return pipettesWithTipAttached -} diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts b/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts deleted file mode 100644 index 99d4ea9abd8..00000000000 --- a/app/src/organisms/DropTipWizardFlows/hooks/useTipAttachmentStatus/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useState, useCallback } from 'react' -import head from 'lodash/head' - -import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { getPipetteModelSpecs } from '@opentrons/shared-data' - -import { getPipettesWithTipAttached } from './getPipettesWithTipAttached' - -import type { Mount } from '@opentrons/api-client' -import type { PipetteModelSpecs } from '@opentrons/shared-data' -import type { GetPipettesWithTipAttached } from './getPipettesWithTipAttached' - -const INSTRUMENTS_POLL_MS = 5000 - -export interface PipetteWithTip { - mount: Mount - specs: PipetteModelSpecs -} - -export interface TipAttachmentStatusResult { - /** Updates the pipettes with tip cache. Determine whether tips are likely attached on one or more pipettes. - * - * NOTE: Use responsibly! This function can potentially (but not likely) iterate over the entire length of a protocol run. - * */ - determineTipStatus: () => Promise - /* Whether tips are likely attached on *any* pipette. Typically called after determineTipStatus() */ - areTipsAttached: boolean - /* Resets the cached pipettes with tip statuses to null. */ - resetTipStatus: () => void - /** Removes the first element from the tip attached cache if present. - * @param {Function} onEmptyCache After removing the pipette from the cache, if the attached tip cache is empty, invoke this callback. - * @param {Function} onTipsDetected After removing the pipette from the cache, if the attached tip cache is not empty, invoke this callback. - * */ - setTipStatusResolved: ( - onEmptyCache?: () => void, - onTipsDetected?: () => void - ) => Promise - /* Relevant pipette information for a pipette with a tip attached. If both pipettes have tips attached, return the left pipette. */ - aPipetteWithTip: PipetteWithTip | null - /* The initial number of pipettes with tips. Null if there has been no tip check yet. */ - initialPipettesWithTipsCount: number | null -} - -// Returns various utilities for interacting with the cache of pipettes with tips attached. -export function useTipAttachmentStatus( - params: Omit -): TipAttachmentStatusResult { - const [pipettesWithTip, setPipettesWithTip] = useState([]) - const [initialPipettesCount, setInitialPipettesCount] = useState< - number | null - >(null) - const { data: attachedInstruments } = useInstrumentsQuery({ - refetchInterval: INSTRUMENTS_POLL_MS, - }) - - const aPipetteWithTip = head(pipettesWithTip) ?? null - const areTipsAttached = - pipettesWithTip.length > 0 && head(pipettesWithTip)?.specs != null - - const determineTipStatus = useCallback((): Promise => { - return getPipettesWithTipAttached({ - ...params, - attachedInstruments: attachedInstruments ?? null, - }).then(pipettesWithTip => { - const pipettesWithTipsData = pipettesWithTip.map(pipette => { - const specs = getPipetteModelSpecs(pipette.instrumentModel) - return { - specs, - mount: pipette.mount, - } - }) - const pipettesWithTipAndSpecs = pipettesWithTipsData.filter( - pipette => pipette.specs != null - ) as PipetteWithTip[] - - setPipettesWithTip(pipettesWithTipAndSpecs) - // Set only once. - if (initialPipettesCount === null) { - setInitialPipettesCount(pipettesWithTipAndSpecs.length) - } - - return Promise.resolve(pipettesWithTipAndSpecs) - }) - }, [params]) - - const resetTipStatus = (): void => { - setPipettesWithTip([]) - setInitialPipettesCount(null) - } - - const setTipStatusResolved = ( - onEmptyCache?: () => void, - onTipsDetected?: () => void - ): Promise => { - return new Promise(resolve => { - setPipettesWithTip(prevPipettesWithTip => { - const newState = [...prevPipettesWithTip.slice(1)] - if (newState.length === 0) { - onEmptyCache?.() - } else { - onTipsDetected?.() - } - - resolve(newState[0]) - return newState - }) - }) - } - - return { - areTipsAttached, - determineTipStatus, - resetTipStatus, - aPipetteWithTip, - setTipStatusResolved, - initialPipettesWithTipsCount: initialPipettesCount, - } -} diff --git a/app/src/organisms/DropTipWizardFlows/index.ts b/app/src/organisms/DropTipWizardFlows/index.ts index 1b53f36e5c8..05a16f92e49 100644 --- a/app/src/organisms/DropTipWizardFlows/index.ts +++ b/app/src/organisms/DropTipWizardFlows/index.ts @@ -1,6 +1,4 @@ export * from './DropTipWizardFlows' -export { useTipAttachmentStatus } from './hooks' export * from './TipsAttachedModal' -export type { TipAttachmentStatusResult, PipetteWithTip } from './hooks' export type { FixitCommandTypeUtils } from './types' diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index 1c62471380d..795b1517ee8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useLayoutEffect } from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -29,7 +29,6 @@ import { import { RecoveryInProgress } from './RecoveryInProgress' import { getErrorKind } from './utils' import { RECOVERY_MAP } from './constants' -import { useHomeGripper } from './hooks' import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' import type { RecoveryRoute, RouteStep, RecoveryContentProps } from './types' @@ -76,23 +75,12 @@ export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps & export function ErrorRecoveryWizard( props: ErrorRecoveryWizardProps ): JSX.Element { - const { - hasLaunchedRecovery, - failedCommand, - recoveryCommands, - routeUpdateActions, - } = props - const errorKind = getErrorKind(failedCommand) - - useInitialPipetteHome({ - hasLaunchedRecovery, - recoveryCommands, - routeUpdateActions, - }) - - useHomeGripper(props) - - return + return ( + + ) } export function ErrorRecoveryComponent( @@ -283,26 +271,3 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return buildSelectRecoveryOption() } } -interface UseInitialPipetteHomeParams { - hasLaunchedRecovery: ErrorRecoveryWizardProps['hasLaunchedRecovery'] - recoveryCommands: ErrorRecoveryWizardProps['recoveryCommands'] - routeUpdateActions: ErrorRecoveryWizardProps['routeUpdateActions'] -} -// Home the Z-axis of all attached pipettes on Error Recovery launch. -export function useInitialPipetteHome({ - hasLaunchedRecovery, - recoveryCommands, - routeUpdateActions, -}: UseInitialPipetteHomeParams): void { - const { homePipetteZAxes } = recoveryCommands - const { handleMotionRouting } = routeUpdateActions - - // Synchronously set the recovery route to "robot in motion" before initial render to prevent screen flicker on ER launch. - useLayoutEffect(() => { - if (hasLaunchedRecovery) { - void handleMotionRouting(true) - .then(() => homePipetteZAxes()) - .finally(() => handleMotionRouting(false)) - } - }, [hasLaunchedRecovery]) -} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx index dd680ed24f6..d2fe92438a4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryError.tsx @@ -92,6 +92,7 @@ export function RecoveryDropTipFlowErrors({ routeUpdateActions, getRecoveryOptionCopy, errorKind, + subMapUtils, }: RecoveryContentProps): JSX.Element { const { t } = useTranslation('error_recovery') const { step } = recoveryMap @@ -108,6 +109,9 @@ export function RecoveryDropTipFlowErrors({ errorKind ) + // Whenever there is an error during drop tip wizard, reset the submap so properly re-entry routing occurs. + subMapUtils.updateSubMap(null) + const buildTitle = (): string => { switch (step) { case ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_GENERAL_ERROR: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx index 3a176942a74..3353c9d4b05 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryInProgress.tsx @@ -101,7 +101,7 @@ export function useGripperRelease({ doorStatusUtils, recoveryMap, }: UseGripperReleaseProps): number { - const { releaseGripperJaws } = recoveryCommands + const { releaseGripperJaws, homeExceptPlungers } = recoveryCommands const { selectedRecoveryOption } = currentRecoveryOptionUtils const { proceedToRouteAndStep, @@ -112,49 +112,47 @@ export function useGripperRelease({ const { MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY } = RECOVERY_MAP const [countdown, setCountdown] = useState(GRIPPER_RELEASE_COUNTDOWN_S) - const proceedToValidNextStep = (): void => { - if (isDoorOpen) { - switch (selectedRecoveryOption) { - case MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - MANUAL_MOVE_AND_SKIP.ROUTE, - MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - break - case MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - MANUAL_REPLACE_AND_RETRY.ROUTE, - MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - break - default: { - console.error( - 'Unhandled post grip-release routing when door is open.' - ) - void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) - } - } - } else { - switch (selectedRecoveryOption) { - case MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - MANUAL_MOVE_AND_SKIP.ROUTE, - MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - break - case MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - MANUAL_REPLACE_AND_RETRY.ROUTE, - MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - break - default: - console.error('Unhandled post grip-release routing.') - void proceedNextStep() + const proceedToDoorStep = (): void => { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: + void proceedToRouteAndStep( + MANUAL_MOVE_AND_SKIP.ROUTE, + MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + ) + break + case MANUAL_REPLACE_AND_RETRY.ROUTE: + void proceedToRouteAndStep( + MANUAL_REPLACE_AND_RETRY.ROUTE, + MANUAL_REPLACE_AND_RETRY.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + ) + break + default: { + console.error('Unhandled post grip-release routing when door is open.') + void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) } } } + const proceedToValidNextStep = (): void => { + switch (selectedRecoveryOption) { + case MANUAL_MOVE_AND_SKIP.ROUTE: + void proceedToRouteAndStep( + MANUAL_MOVE_AND_SKIP.ROUTE, + MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE + ) + break + case MANUAL_REPLACE_AND_RETRY.ROUTE: + void proceedToRouteAndStep( + MANUAL_REPLACE_AND_RETRY.ROUTE, + MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE + ) + break + default: + console.error('Unhandled post grip-release routing.') + void proceedNextStep() + } + } + useEffect(() => { let intervalId: NodeJS.Timeout | null = null @@ -167,11 +165,21 @@ export function useGripperRelease({ if (intervalId != null) { clearInterval(intervalId) } - void releaseGripperJaws() - .finally(() => handleMotionRouting(false)) - .then(() => { - proceedToValidNextStep() - }) + + void releaseGripperJaws().then(() => { + if (isDoorOpen) { + return handleMotionRouting(false).then(() => { + proceedToDoorStep() + }) + } + + return handleMotionRouting(true) + .then(() => homeExceptPlungers()) + .then(() => handleMotionRouting(false)) + .then(() => { + proceedToValidNextStep() + }) + }) } return updatedCountdown diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 0cd49fec3ea..1609acfa0ca 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -24,10 +24,8 @@ import { DT_ROUTES } from '/app/organisms/DropTipWizardFlows/constants' import { SelectRecoveryOption } from './SelectRecoveryOption' import type { RecoveryContentProps, RecoveryRoute, RouteStep } from '../types' -import type { - FixitCommandTypeUtils, - PipetteWithTip, -} from '/app/organisms/DropTipWizardFlows' +import type { FixitCommandTypeUtils } from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' // The Drop Tip flow entry point. Includes entry from SelectRecoveryOption and CancelRun. export function ManageTips(props: RecoveryContentProps): JSX.Element { @@ -202,6 +200,7 @@ export function useDropTipFlowUtils({ subMapUtils, routeUpdateActions, recoveryMap, + errorKind, }: RecoveryContentProps): FixitCommandTypeUtils { const { t } = useTranslation('error_recovery') const { @@ -210,7 +209,7 @@ export function useDropTipFlowUtils({ ERROR_WHILE_RECOVERING, DROP_TIP_FLOWS, } = RECOVERY_MAP - const { runId } = tipStatusUtils + const { runId, gripperErrorFirstPipetteWithTip } = tipStatusUtils const { step } = recoveryMap const { selectedRecoveryOption } = currentRecoveryOptionUtils const { proceedToRouteAndStep } = routeUpdateActions @@ -304,11 +303,12 @@ export function useDropTipFlowUtils({ } const pipetteId = - failedCommand != null && + gripperErrorFirstPipetteWithTip ?? + (failedCommand != null && 'params' in failedCommand.byRunRecord && 'pipetteId' in failedCommand.byRunRecord.params ? failedCommand.byRunRecord.params.pipetteId - : null + : null) return { runId, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index c44252e2da9..59888c39c42 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -20,7 +20,7 @@ import { import { RecoverySingleColumnContentWrapper } from '../shared' import type { ErrorKind, RecoveryContentProps, RecoveryRoute } from '../types' -import type { PipetteWithTip } from '/app/organisms/DropTipWizardFlows' +import type { PipetteWithTip } from '/app/resources/instruments' // The "home" route within Error Recovery. When a user completes a non-terminal flow or presses "Go back" enough // to escape the boundaries of any route, they will be redirected here. diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx index 7a17b443508..cd31843f834 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx @@ -83,13 +83,14 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { runStatus, recoveryActionMutationUtils, resumePausedRecovery, + recoveryCommands, } = props const { t } = useTranslation('error_recovery') const errorKind = getErrorKind(failedCommand) const title = useErrorName(errorKind) const { makeToast } = useToaster() - const { proceedToRouteAndStep } = routeUpdateActions + const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { reportErrorEvent } = analytics const buildTitleHeadingDesktop = (): JSX.Element => { @@ -138,9 +139,16 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { const onLaunchERClick = (): void => { const onClick = (): void => { - void toggleERWizAsActiveUser(true, true).then(() => { - reportErrorEvent(failedCommand?.byRunRecord ?? null, 'launch-recovery') - }) + void toggleERWizAsActiveUser(true, true) + .then(() => { + reportErrorEvent( + failedCommand?.byRunRecord ?? null, + 'launch-recovery' + ) + }) + .then(() => handleMotionRouting(true)) + .then(() => recoveryCommands.homePipetteZAxes()) + .finally(() => handleMotionRouting(false)) } handleConditionalClick(onClick) } diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index 62fb2849753..dd915b72afb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -1,13 +1,12 @@ import type * as React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' -import { renderHook, act, screen, waitFor } from '@testing-library/react' +import { renderHook, act, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { i18n } from '/app/i18n' import { mockRecoveryContentProps } from '../__fixtures__' import { ErrorRecoveryContent, - useInitialPipetteHome, useERWizard, ErrorRecoveryComponent, } from '../ErrorRecoveryWizard' @@ -35,8 +34,6 @@ import { RecoveryDoorOpenSpecial, } from '../shared' -import type { Mock } from 'vitest' - vi.mock('../RecoveryOptions') vi.mock('../RecoveryInProgress') vi.mock('../RecoveryError') @@ -509,73 +506,3 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_DOOR_OPEN_SPECIAL') }) }) - -describe('useInitialPipetteHome', () => { - let mockZHomePipetteZAxes: Mock - let mockhandleMotionRouting: Mock - let mockRecoveryCommands: any - let mockRouteUpdateActions: any - - beforeEach(() => { - mockZHomePipetteZAxes = vi.fn() - mockhandleMotionRouting = vi.fn() - - mockhandleMotionRouting.mockResolvedValue(() => mockZHomePipetteZAxes()) - mockZHomePipetteZAxes.mockResolvedValue(() => mockhandleMotionRouting()) - - mockRecoveryCommands = { - homePipetteZAxes: mockZHomePipetteZAxes, - } as any - mockRouteUpdateActions = { - handleMotionRouting: mockhandleMotionRouting, - } as any - }) - - it('does not z-home the pipettes if error recovery was not launched', () => { - renderHook(() => - useInitialPipetteHome({ - hasLaunchedRecovery: false, - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - }) - ) - - expect(mockhandleMotionRouting).not.toHaveBeenCalled() - }) - - it('sets the motion screen properly and z-homes all pipettes only on the initial render of Error Recovery', async () => { - const { rerender } = renderHook(() => - useInitialPipetteHome({ - hasLaunchedRecovery: true, - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - }) - ) - - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledWith(true) - }) - await waitFor(() => { - expect(mockZHomePipetteZAxes).toHaveBeenCalledTimes(1) - }) - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledWith(false) - }) - - expect(mockhandleMotionRouting.mock.invocationCallOrder[0]).toBeLessThan( - mockZHomePipetteZAxes.mock.invocationCallOrder[0] - ) - expect(mockZHomePipetteZAxes.mock.invocationCallOrder[0]).toBeLessThan( - mockhandleMotionRouting.mock.invocationCallOrder[1] - ) - - rerender() - - await waitFor(() => { - expect(mockhandleMotionRouting).toHaveBeenCalledTimes(2) - }) - await waitFor(() => { - expect(mockZHomePipetteZAxes).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx index 1394993746b..f46f3f949ba 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryError.test.tsx @@ -25,12 +25,14 @@ describe('RecoveryError', () => { let getRecoverOptionCopyMock: Mock let handleMotionRoutingMock: Mock let homePipetteZAxesMock: Mock + let updateSubMapMock: Mock beforeEach(() => { proceedToRouteAndStepMock = vi.fn() getRecoverOptionCopyMock = vi.fn() handleMotionRoutingMock = vi.fn().mockResolvedValue(undefined) homePipetteZAxesMock = vi.fn().mockResolvedValue(undefined) + updateSubMapMock = vi.fn() props = { ...mockRecoveryContentProps, @@ -48,6 +50,7 @@ describe('RecoveryError', () => { route: ERROR_WHILE_RECOVERING.ROUTE, step: ERROR_WHILE_RECOVERING.STEPS.RECOVERY_ACTION_FAILED, }, + subMapUtils: { subMap: null, updateSubMap: updateSubMapMock }, } getRecoverOptionCopyMock.mockReturnValue('Retry step') @@ -95,7 +98,7 @@ describe('RecoveryError', () => { expect(screen.queryAllByText('Continue to drop tip')[0]).toBeInTheDocument() }) - it(`renders RecoveryDropTipFlowErrors when step is ${ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED}`, () => { + it(`renders RecoveryDropTipFlowErrors when step is ${ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED} and resets the submap`, () => { props.recoveryMap.step = RECOVERY_MAP.ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED render(props) @@ -107,6 +110,7 @@ describe('RecoveryError', () => { )[0] ).toBeInTheDocument() expect(screen.queryAllByText('Return to menu')[0]).toBeInTheDocument() + expect(updateSubMapMock).toHaveBeenCalledWith(null) }) it(`calls proceedToRouteAndStep with ${RECOVERY_MAP.OPTION_SELECTION.ROUTE} when the "Return to menu" button is clicked in RecoveryDropTipFlowErrors with step ${ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_GENERAL_ERROR}`, () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx index c3005c10cda..2dfa5711644 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoveryInProgress.test.tsx @@ -39,10 +39,12 @@ describe('RecoveryInProgress', () => { }, recoveryCommands: { releaseGripperJaws: vi.fn(() => Promise.resolve()), + homeExceptPlungers: vi.fn(() => Promise.resolve()), } as any, routeUpdateActions: { handleMotionRouting: vi.fn(() => Promise.resolve()), proceedNextStep: vi.fn(() => Promise.resolve()), + proceedToRouteAndStep: vi.fn(() => Promise.resolve()), } as any, } }) @@ -166,14 +168,12 @@ describe('useGripperRelease', () => { }, recoveryCommands: { releaseGripperJaws: vi.fn().mockResolvedValue(undefined), + homeExceptPlungers: vi.fn().mockResolvedValue(undefined), }, routeUpdateActions: { - proceedToRouteAndStep: vi.fn(), - proceedNextStep: vi.fn(), - handleMotionRouting: vi.fn(), - stashedMap: { - route: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - }, + proceedToRouteAndStep: vi.fn().mockResolvedValue(undefined), + proceedNextStep: vi.fn().mockResolvedValue(undefined), + handleMotionRouting: vi.fn().mockResolvedValue(undefined), }, currentRecoveryOptionUtils: { selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, @@ -183,6 +183,7 @@ describe('useGripperRelease', () => { beforeEach(() => { vi.useFakeTimers() + vi.clearAllMocks() }) afterEach(() => { @@ -207,118 +208,143 @@ describe('useGripperRelease', () => { expect(result.current).toBe(0) }) - const IS_DOOR_OPEN = [false, true] - - IS_DOOR_OPEN.forEach(doorStatus => { - it(`releases gripper jaws and proceeds to next step after countdown for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE} when the isDoorOpen is ${doorStatus}`, async () => { - renderHook(() => - useGripperRelease({ + describe('when door is closed', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + nextStep: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + nextStep: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, + }, + ])( + 'executes the full sequence of commands for $recoveryOption', + async ({ recoveryOption, nextStep }) => { + const props = { ...mockProps, - doorStatusUtils: { isDoorOpen: doorStatus }, - }) - ) - - act(() => { - vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) - }) + currentRecoveryOptionUtils: { + selectedRecoveryOption: recoveryOption, + }, + doorStatusUtils: { isDoorOpen: false }, + } - await vi.runAllTimersAsync() + renderHook(() => useGripperRelease(props)) - expect(mockProps.recoveryCommands.releaseGripperJaws).toHaveBeenCalled() - expect( - mockProps.routeUpdateActions.handleMotionRouting - ).toHaveBeenCalledWith(false) - if (!doorStatus) { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - } else { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() + + const { + releaseGripperJaws, + homeExceptPlungers, + } = props.recoveryCommands + const { + handleMotionRouting, + proceedToRouteAndStep, + } = props.routeUpdateActions + + expect(releaseGripperJaws).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(1, true) + expect(homeExceptPlungers).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(2, false) + expect(proceedToRouteAndStep).toHaveBeenCalledWith( + recoveryOption, + nextStep ) } + ) + + describe('when door is open', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + doorStep: + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + doorStep: + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS + .CLOSE_DOOR_GRIPPER_Z_HOME, + }, + ])( + 'executes proceed to door step for $recoveryOption', + async ({ recoveryOption, doorStep }) => { + const props = { + ...mockProps, + currentRecoveryOptionUtils: { + selectedRecoveryOption: recoveryOption, + }, + doorStatusUtils: { isDoorOpen: true }, + } + + const { + releaseGripperJaws, + homeExceptPlungers, + } = props.recoveryCommands + const { + handleMotionRouting, + proceedToRouteAndStep, + } = props.routeUpdateActions + + renderHook(() => useGripperRelease(props)) + + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() + + expect(releaseGripperJaws).toHaveBeenCalledTimes(1) + expect(handleMotionRouting).toHaveBeenNthCalledWith(1, false) + expect(homeExceptPlungers).not.toHaveBeenCalled() + expect(proceedToRouteAndStep).toHaveBeenCalledWith( + recoveryOption, + doorStep + ) + } + ) }) - }) - IS_DOOR_OPEN.forEach(doorStatus => { - it(`releases gripper jaws and proceeds to next step after countdown for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE} when the isDoorOpen is ${doorStatus}`, async () => { - const modifiedProps = { + it('falls back to option selection for unhandled routes when door is open', async () => { + const props = { ...mockProps, - routeUpdateActions: { - ...mockProps.routeUpdateActions, - stashedMap: { - route: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - }, - }, currentRecoveryOptionUtils: { - selectedRecoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + selectedRecoveryOption: 'UNHANDLED_ROUTE', }, + doorStatusUtils: { isDoorOpen: true }, } - renderHook(() => - useGripperRelease({ - ...modifiedProps, - doorStatusUtils: { isDoorOpen: doorStatus }, - }) - ) + renderHook(() => useGripperRelease(props)) act(() => { vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) }) - await vi.runAllTimersAsync() - expect(mockProps.recoveryCommands.releaseGripperJaws).toHaveBeenCalled() expect( - mockProps.routeUpdateActions.handleMotionRouting - ).toHaveBeenCalledWith(false) - if (!doorStatus) { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - } else { - expect( - mockProps.routeUpdateActions.proceedToRouteAndStep - ).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.CLOSE_DOOR_GRIPPER_Z_HOME - ) - } + props.routeUpdateActions.proceedToRouteAndStep + ).toHaveBeenCalledWith(RECOVERY_MAP.OPTION_SELECTION.ROUTE) }) - }) - it('calls proceedNextStep for unhandled routes', async () => { - const modifiedProps = { - ...mockProps, - routeUpdateActions: { - ...mockProps.routeUpdateActions, - stashedMap: { - route: 'UNHANDLED_ROUTE', + it('falls back to proceedNextStep for unhandled routes when door is closed', async () => { + const props = { + ...mockProps, + currentRecoveryOptionUtils: { + selectedRecoveryOption: 'UNHANDLED_ROUTE', }, - }, - currentRecoveryOptionUtils: { - selectedRecoveryOption: RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, - }, - doorStatusUtils: { isDoorOpen: false }, - } - - renderHook(() => useGripperRelease(modifiedProps)) + doorStatusUtils: { isDoorOpen: false }, + } - act(() => { - vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) - }) + renderHook(() => useGripperRelease(props)) - await vi.runAllTimersAsync() + act(() => { + vi.advanceTimersByTime(GRIPPER_RELEASE_COUNTDOWN_S * 1000) + }) + await vi.runAllTimersAsync() - expect(modifiedProps.routeUpdateActions.proceedNextStep).toHaveBeenCalled() + expect(props.routeUpdateActions.proceedNextStep).toHaveBeenCalled() + }) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx index da3ddc07629..7446ea7464c 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/RecoverySplash.test.tsx @@ -80,11 +80,14 @@ describe('RecoverySplash', () => { let props: React.ComponentProps const mockToggleERWiz = vi.fn(() => Promise.resolve()) const mockProceedToRouteAndStep = vi.fn() + const mockHandleMotionRouting = vi.fn(() => Promise.resolve()) const mockRouteUpdateActions = { proceedToRouteAndStep: mockProceedToRouteAndStep, + handleMotionRouting: mockHandleMotionRouting, } as any const mockMakeToast = vi.fn() const mockResumeRecovery = vi.fn() + const mockHomePipetteZAxes = vi.fn(() => Promise.resolve()) beforeEach(() => { props = { @@ -96,6 +99,7 @@ describe('RecoverySplash', () => { resumeRecovery: mockResumeRecovery, } as any, resumePausedRecovery: true, + recoveryCommands: { homePipetteZAxes: mockHomePipetteZAxes } as any, } vi.mocked(StepInfo).mockReturnValue(
MOCK STEP INFO
) @@ -162,6 +166,13 @@ describe('RecoverySplash', () => { await waitFor(() => { expect(mockToggleERWiz).toHaveBeenCalledWith(true, true) }) + + expect(mockHandleMotionRouting).toHaveBeenNthCalledWith(1, true) + expect(mockHandleMotionRouting).toHaveBeenNthCalledWith(2, false) + + await waitFor(() => { + expect(props.recoveryCommands.homePipetteZAxes).toHaveBeenCalled() + }) }) it('should render a door open toast if the door is open', () => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts deleted file mode 100644 index 32de0f0096d..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useHomeGripper.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { renderHook, act } from '@testing-library/react' -import { describe, it, expect, vi, beforeEach } from 'vitest' - -import { useHomeGripper } from '../useHomeGripper' -import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' - -describe('useHomeGripper', () => { - const mockRecoveryCommands = { - homeExceptPlungers: vi.fn().mockResolvedValue(undefined), - } - - const mockRouteUpdateActions = { - handleMotionRouting: vi.fn().mockResolvedValue(undefined), - goBackPrevStep: vi.fn(), - } - - const mockRecoveryMap = { - step: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, - } - - const mockDoorStatusUtils = { - isDoorOpen: false, - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should home gripper Z axis when in manual gripper step and door is closed', async () => { - renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRouteUpdateActions.handleMotionRouting).toHaveBeenCalledWith( - true - ) - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalled() - expect(mockRouteUpdateActions.handleMotionRouting).toHaveBeenCalledWith( - false - ) - }) - - it('should go back to previous step when door is open', () => { - renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: { ...mockDoorStatusUtils, isDoorOpen: true }, - } as any) - }) - - expect(mockRouteUpdateActions.goBackPrevStep).toHaveBeenCalled() - expect(mockRecoveryCommands.homeExceptPlungers).not.toHaveBeenCalled() - }) - - it('should not home again if already homed once', async () => { - const { rerender } = renderHook(() => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap: mockRecoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender() - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - }) - - it('should only reset hasHomedOnce when step changes to non-manual gripper step', async () => { - const { rerender } = renderHook( - ({ recoveryMap }) => { - useHomeGripper({ - recoveryCommands: mockRecoveryCommands, - routeUpdateActions: mockRouteUpdateActions, - recoveryMap, - doorStatusUtils: mockDoorStatusUtils, - } as any) - }, - { - initialProps: { recoveryMap: mockRecoveryMap }, - } - ) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender({ recoveryMap: { step: 'SOME_OTHER_STEP' } as any }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - - rerender({ recoveryMap: mockRecoveryMap }) - - await act(async () => { - await new Promise(resolve => setTimeout(resolve, 0)) - }) - - expect(mockRecoveryCommands.homeExceptPlungers).toHaveBeenCalledTimes(1) - }) -}) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts index ca2e086d9fd..21307b8e4e8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts @@ -139,24 +139,41 @@ describe('useRecoveryCommands', () => { false ) }) - ;([ + + const IN_PLACE_COMMANDS = [ 'aspirateInPlace', 'dispenseInPlace', 'blowOutInPlace', 'dropTipInPlace', 'prepareToAspirate', - ] as const).forEach(inPlaceCommandType => { - it(`Should move to retryLocation if failed command is ${inPlaceCommandType} and error is appropriate when retrying`, async () => { + ] as const + + const ERROR_SCENARIOS = [ + { type: 'overpressure', code: '3006' }, + { type: 'tipPhysicallyAttached', code: '3007' }, + ] as const + + it.each( + ERROR_SCENARIOS.flatMap(error => + IN_PLACE_COMMANDS.map(commandType => ({ + errorType: error.type, + errorCode: error.code, + commandType, + })) + ) + )( + 'Should move to retryLocation if failed command is $commandType and error is $errorType when retrying', + async ({ errorType, errorCode, commandType }) => { const { result } = renderHook(() => { const failedCommand = { ...mockFailedCommand, - commandType: inPlaceCommandType, + commandType, params: { pipetteId: 'mock-pipette-id', }, error: { - errorType: 'overpressure', - errorCode: '3006', + errorType, + errorCode, isDefined: true, errorInfo: { retryLocation: [1, 2, 3], @@ -180,9 +197,11 @@ describe('useRecoveryCommands', () => { selectedRecoveryOption: RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE, }) }) + await act(async () => { await result.current.retryFailedCommand() }) + expect(mockChainRunCommands).toHaveBeenLastCalledWith( [ { @@ -194,14 +213,14 @@ describe('useRecoveryCommands', () => { }, }, { - commandType: inPlaceCommandType, + commandType, params: { pipetteId: 'mock-pipette-id' }, }, ], false ) - }) - }) + } + ) it('should call resumeRun with runId and show success toast on success', async () => { const { result } = renderHook(() => useRecoveryCommands(props)) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts index 75904a24966..497fd3223da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/index.ts @@ -5,7 +5,6 @@ export { useRouteUpdateActions } from './useRouteUpdateActions' export { useERUtils } from './useERUtils' export { useRecoveryTakeover } from './useRecoveryTakeover' export { useRetainedFailedCommandBySource } from './useRetainedFailedCommandBySource' -export { useHomeGripper } from './useHomeGripper' export type { ERUtilsProps } from './useERUtils' export type { UseRouteUpdateActionsResult } from './useRouteUpdateActions' diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index 533b30aa6c4..ecf03e4f56b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -139,6 +139,7 @@ export function useERUtils({ const tipStatusUtils = useRecoveryTipStatus({ runId, runRecord, + failedCommand, attachedInstruments, failedPipetteInfo, }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts deleted file mode 100644 index 55fe64fdcc4..00000000000 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useHomeGripper.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useLayoutEffect, useState } from 'react' -import { RECOVERY_MAP } from '/app/organisms/ErrorRecoveryFlows/constants' - -import type { ErrorRecoveryWizardProps } from '/app/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard' - -// Home the gripper implicitly. Because the home is not tied to a CTA, it must be handled here. -export function useHomeGripper({ - recoveryCommands, - routeUpdateActions, - recoveryMap, - doorStatusUtils, -}: ErrorRecoveryWizardProps): void { - const { step } = recoveryMap - const { isDoorOpen } = doorStatusUtils - const [hasHomedOnce, setHasHomedOnce] = useState(false) - - const isManualGripperStep = - step === RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE || - step === RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - - useLayoutEffect(() => { - const { handleMotionRouting, goBackPrevStep } = routeUpdateActions - const { homeExceptPlungers } = recoveryCommands - - if (!hasHomedOnce) { - if (isManualGripperStep) { - if (isDoorOpen) { - void goBackPrevStep() - } else { - void handleMotionRouting(true) - .then(() => homeExceptPlungers()) - .then(() => handleMotionRouting(false)) - .then(() => { - setHasHomedOnce(true) - }) - } - } - } - }, [step, hasHomedOnce, isDoorOpen, isManualGripperStep]) -} diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 4ce5194aca4..65bd77eed0b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -9,7 +9,7 @@ import { } from '@opentrons/react-api-client' import { useChainRunCommands } from '/app/resources/runs' -import { ERROR_KINDS, RECOVERY_MAP } from '../constants' +import { DEFINED_ERROR_TYPES, ERROR_KINDS, RECOVERY_MAP } from '../constants' import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' import type { @@ -127,6 +127,7 @@ export function useRecoveryCommands({ | DispenseInPlaceRunTimeCommand | DropTipInPlaceRunTimeCommand | PrepareToAspirateRunTimeCommand + const IN_PLACE_COMMAND_TYPES = [ 'aspirateInPlace', 'dispenseInPlace', @@ -134,16 +135,25 @@ export function useRecoveryCommands({ 'dropTipInPlace', 'prepareToAspirate', ] as const + + const RETRY_ERROR_TYPES = [ + DEFINED_ERROR_TYPES.OVERPRESSURE, + DEFINED_ERROR_TYPES.TIP_PHYSICALLY_ATTACHED, + ] as const + const isInPlace = ( failedCommand: FailedCommand ): failedCommand is InPlaceCommand => IN_PLACE_COMMAND_TYPES.includes( (failedCommand as InPlaceCommand).commandType ) + return unvalidatedFailedCommand != null ? isInPlace(unvalidatedFailedCommand) ? unvalidatedFailedCommand.error?.isDefined && - unvalidatedFailedCommand.error?.errorType === 'overpressure' && + RETRY_ERROR_TYPES.includes( + unvalidatedFailedCommand.error?.errorType + ) && // Paranoia: this value comes from the wire and may be unevenly implemented typeof unvalidatedFailedCommand.error?.errorInfo?.retryLocation?.at( 0 diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts index a715d12c83f..8db4af030ea 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts @@ -1,18 +1,22 @@ import { useState } from 'react' import head from 'lodash/head' -import { useHost } from '@opentrons/react-api-client' +import { useRunCurrentState } from '@opentrons/react-api-client' import { getPipetteModelSpecs } from '@opentrons/shared-data' -import { useTipAttachmentStatus } from '/app/organisms/DropTipWizardFlows' +import { useTipAttachmentStatus } from '/app/resources/instruments' +import { ERROR_KINDS } from '/app/organisms/ErrorRecoveryFlows/constants' +import { getErrorKind } from '/app/organisms/ErrorRecoveryFlows/utils' import type { Run, Instruments, PipetteData } from '@opentrons/api-client' import type { PipetteWithTip, TipAttachmentStatusResult, -} from '/app/organisms/DropTipWizardFlows' +} from '/app/resources/instruments' +import type { ERUtilsProps } from '/app/organisms/ErrorRecoveryFlows/hooks/useERUtils' interface UseRecoveryTipStatusProps { runId: string + failedCommand: ERUtilsProps['failedCommand'] failedPipetteInfo: PipetteData | null attachedInstruments?: Instruments runRecord?: Run @@ -22,6 +26,7 @@ export type RecoveryTipStatusUtils = TipAttachmentStatusResult & { /* Whether the robot is currently determineTipStatus() */ isLoadingTipStatus: boolean runId: string + gripperErrorFirstPipetteWithTip: string | null } // Wraps the tip attachment status utils with Error Recovery specific states and values. @@ -33,11 +38,9 @@ export function useRecoveryTipStatus( failedCommandPipette, setFailedCommandPipette, ] = useState(null) - const host = useHost() const tipAttachmentStatusUtils = useTipAttachmentStatus({ ...props, - host, runRecord: props.runRecord ?? null, }) @@ -77,11 +80,26 @@ export function useRecoveryTipStatus( }) } + // TODO(jh, 11-15-24): This is temporary. Collaborate with design a better way to do drop tip wizard for multiple + // pipettes during error recovery. The tip detection logic will shortly be simplified, too! + const errorKind = getErrorKind(props.failedCommand) + const currentTipStates = + useRunCurrentState(props.runId, { + enabled: errorKind === ERROR_KINDS.GRIPPER_ERROR, + }).data?.data.tipStates ?? null + + const gripperErrorFirstPipetteWithTip = + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.entries(currentTipStates ?? {}).find( + ([_, state]) => state.hasTip + )?.[0] ?? null + return { ...tipAttachmentStatusUtils, aPipetteWithTip: failedCommandPipette, determineTipStatus: determineTipStatusWithLoading, isLoadingTipStatus, runId: props.runId, + gripperErrorFirstPipetteWithTip, } } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx index 4331a976d5e..98744985225 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -20,7 +20,11 @@ import { RecoverySingleColumnContentWrapper } from './RecoveryContentWrapper' import { RecoveryFooterButtons } from './RecoveryFooterButtons' import { RECOVERY_MAP } from '../constants' -import type { RecoveryContentProps } from '../../ErrorRecoveryFlows/types' +import type { + RecoveryContentProps, + RecoveryRoute, + RouteStep, +} from '../../ErrorRecoveryFlows/types' // Whenever a step uses a custom "close the robot door" view, use this component. // Note that the allowDoorOpen metadata for the route must be set to true for this view to render. @@ -30,9 +34,11 @@ export function RecoveryDoorOpenSpecial({ recoveryActionMutationUtils, routeUpdateActions, doorStatusUtils, + recoveryCommands, }: RecoveryContentProps): JSX.Element { const { selectedRecoveryOption } = currentRecoveryOptionUtils const { resumeRecovery } = recoveryActionMutationUtils + const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { t } = useTranslation('error_recovery') const [isLoading, setIsLoading] = useState(false) @@ -56,29 +62,40 @@ export function RecoveryDoorOpenSpecial({ } } - if (!doorStatusUtils.isDoorOpen) { - const { proceedToRouteAndStep } = routeUpdateActions - switch (selectedRecoveryOption) { - case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: - void proceedToRouteAndStep( - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - break - case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: - void proceedToRouteAndStep( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - break - default: { - console.error( - `Unhandled special-cased door open on route ${selectedRecoveryOption}.` - ) - void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) + const handleHomeExceptPlungersAndRoute = ( + route: RecoveryRoute, + step?: RouteStep + ): void => { + void handleMotionRouting(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + .then(() => recoveryCommands.homeExceptPlungers()) + .finally(() => handleMotionRouting(false)) + .then(() => proceedToRouteAndStep(route, step)) + } + + useEffect(() => { + if (!doorStatusUtils.isDoorOpen) { + switch (selectedRecoveryOption) { + case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: + handleHomeExceptPlungersAndRoute( + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE + ) + break + case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: + handleHomeExceptPlungersAndRoute( + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE + ) + break + default: { + console.error( + `Unhandled special-cased door open on route ${selectedRecoveryOption}.` + ) + void proceedToRouteAndStep(RECOVERY_MAP.OPTION_SELECTION.ROUTE) + } } } - } + }, [doorStatusUtils.isDoorOpen]) return ( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx index 76e42a04c6d..5cc4ae74b87 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/RecoveryDoorOpenSpecial.test.tsx @@ -1,5 +1,5 @@ import { describe, it, vi, expect, beforeEach } from 'vitest' -import { screen } from '@testing-library/react' +import { screen, waitFor } from '@testing-library/react' import { RUN_STATUS_AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR, @@ -28,10 +28,14 @@ describe('RecoveryDoorOpenSpecial', () => { }, routeUpdateActions: { proceedToRouteAndStep: vi.fn(), + handleMotionRouting: vi.fn().mockImplementation(_ => Promise.resolve()), }, doorStatusUtils: { isDoorOpen: true, }, + recoveryCommands: { + homeExceptPlungers: vi.fn().mockResolvedValue(undefined), + }, } as any }) @@ -70,7 +74,50 @@ describe('RecoveryDoorOpenSpecial', () => { ) }) - it('renders default subtext for unhandled recovery option', () => { + it.each([ + { + recoveryOption: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + expectedRoute: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, + expectedStep: RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE, + }, + { + recoveryOption: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + expectedRoute: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, + expectedStep: RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE, + }, + ])( + 'executes correct chain of actions when door is closed for $recoveryOption', + async ({ recoveryOption, expectedRoute, expectedStep }) => { + props.currentRecoveryOptionUtils.selectedRecoveryOption = recoveryOption + props.doorStatusUtils.isDoorOpen = false + + render(props) + + await waitFor(() => { + expect( + props.routeUpdateActions.handleMotionRouting + ).toHaveBeenCalledWith(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + }) + + await waitFor(() => { + expect(props.recoveryCommands.homeExceptPlungers).toHaveBeenCalled() + }) + + await waitFor(() => { + expect( + props.routeUpdateActions.handleMotionRouting + ).toHaveBeenCalledWith(false) + }) + + await waitFor(() => { + expect( + props.routeUpdateActions.proceedToRouteAndStep + ).toHaveBeenCalledWith(expectedRoute, expectedStep) + }) + } + ) + + it('renders default subtext for an unhandled recovery option', () => { props.currentRecoveryOptionUtils.selectedRecoveryOption = 'UNHANDLED_OPTION' as any render(props) screen.getByText('Close the robot door') @@ -79,26 +126,6 @@ describe('RecoveryDoorOpenSpecial', () => { ) }) - it(`calls proceedToRouteAndStep when door is closed for ${RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE}`, () => { - props.doorStatusUtils.isDoorOpen = false - render(props) - expect(props.routeUpdateActions.proceedToRouteAndStep).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE, - RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.STEPS.MANUAL_REPLACE - ) - }) - - it(`calls proceedToRouteAndStep when door is closed for ${RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE}`, () => { - props.currentRecoveryOptionUtils.selectedRecoveryOption = - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE - props.doorStatusUtils.isDoorOpen = false - render(props) - expect(props.routeUpdateActions.proceedToRouteAndStep).toHaveBeenCalledWith( - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE, - RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE - ) - }) - it('calls proceedToRouteAndStep with OPTION_SELECTION for unhandled recovery option when door is closed', () => { props.currentRecoveryOptionUtils.selectedRecoveryOption = 'UNHANDLED_OPTION' as any props.doorStatusUtils.isDoorOpen = false diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index 34f07d39a35..1a4bab512eb 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -71,6 +71,7 @@ export function GripperWizardFlows( const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = useState< string | null >(null) + const [errorMessage, setErrorMessage] = useState(null) // we should start checking for run deletion only after the maintenance run is created // and the useCurrentRun poll has returned that created id @@ -86,6 +87,9 @@ export function GripperWizardFlows( onSuccess: response => { setCreatedMaintenanceRunId(response.data.id) }, + onError: error => { + setErrorMessage(error.message) + }, }) const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ @@ -117,7 +121,6 @@ export function GripperWizardFlows( ]) const [isExiting, setIsExiting] = useState(false) - const [errorMessage, setErrorMessage] = useState(null) const handleClose = (): void => { if (props?.onComplete != null) { @@ -298,9 +301,12 @@ export const GripperWizard = ( isRobotMoving={isRobotMoving} /> ) - } else if ( + } + // These flows often have custom error messaging, so this fallback modal is shown only in specific circumstances. + else if ( (isExiting && errorMessage != null) || - maintenanceRunStatus === RUN_STATUS_FAILED + maintenanceRunStatus === RUN_STATUS_FAILED || + (errorMessage != null && createdMaintenanceRunId == null) ) { onExit = handleClose modalContent = ( diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts b/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts index 8c2831115c9..4e8119e4d74 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts @@ -3,7 +3,6 @@ import { HEATERSHAKER_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, ABSORBANCE_READER_TYPE, - NON_USER_ADDRESSABLE_LABWARE, } from '@opentrons/shared-data' import type { @@ -49,8 +48,7 @@ export function getPrepCommands( return [...acc, loadWithPipetteId] } else if ( command.commandType === 'loadLabware' && - command.result?.labwareId != null && - !NON_USER_ADDRESSABLE_LABWARE.includes(command.params.loadName) + command.result?.labwareId != null ) { // load all labware off-deck so that LPC can move them on individually later return [ diff --git a/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx b/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx index a22c7591360..a668fc48e70 100644 --- a/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx +++ b/app/src/organisms/ModuleCard/ModuleOverflowMenu.tsx @@ -150,7 +150,7 @@ export const ModuleOverflowMenu = ( item.onClick(item.isSecondary)} - disabled={item.disabledReason || isDisabled} + disabled={item.isSettingDisabled} whiteSpace={NO_WRAP} > {item.setSetting} diff --git a/app/src/organisms/ModuleCard/hooks.tsx b/app/src/organisms/ModuleCard/hooks.tsx index 10b0e9fd30b..da44c64d983 100644 --- a/app/src/organisms/ModuleCard/hooks.tsx +++ b/app/src/organisms/ModuleCard/hooks.tsx @@ -80,6 +80,7 @@ export function useLatchControls(module: AttachedModule): LatchControls { export type MenuItemsByModuleType = { [moduleType in AttachedModule['moduleType']]: Array<{ setSetting: string + isSettingDisabled: boolean isSecondary: boolean menuButtons: JSX.Element[] | null onClick: (isSecondary: boolean) => void @@ -267,6 +268,7 @@ export function useModuleOverflowMenu( module.data.lidTargetTemperature != null ? t('overflow_menu_deactivate_lid') : t('overflow_menu_lid_temp'), + isSettingDisabled: isDisabled, isSecondary: true, menuButtons: null, onClick: @@ -285,6 +287,7 @@ export function useModuleOverflowMenu( module.data.lidStatus === 'open' ? t('close_lid') : t('open_lid'), + isSettingDisabled: isDisabled, isSecondary: false, menuButtons: [thermoSetBlockTempBtn, aboutModuleBtn], onClick: controlTCLid, @@ -298,6 +301,7 @@ export function useModuleOverflowMenu( ? t('overflow_menu_deactivate_temp') : t('overflow_menu_mod_temp'), isSecondary: false, + isSettingDisabled: isDisabled, menuButtons: [aboutModuleBtn], onClick: module.data.status !== 'idle' @@ -317,6 +321,7 @@ export function useModuleOverflowMenu( ? t('overflow_menu_disengage') : t('overflow_menu_engage'), isSecondary: false, + isSettingDisabled: isDisabled, menuButtons: [aboutModuleBtn], onClick: module.data.status !== 'disengaged' @@ -336,6 +341,7 @@ export function useModuleOverflowMenu( ? t('heater_shaker:deactivate_heater') : t('heater_shaker:set_temperature'), isSecondary: false, + isSettingDisabled: isDisabled, menuButtons: [ labwareLatchBtn, aboutModuleBtn, @@ -358,6 +364,7 @@ export function useModuleOverflowMenu( { setSetting: t('overflow_menu_about'), isSecondary: false, + isSettingDisabled: false, menuButtons: [], onClick: handleAboutClick, }, diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx index d8dcf237caa..bcaaca86182 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ChooseCsvFile.tsx @@ -11,8 +11,8 @@ import { DIRECTION_ROW, Flex, LegacyStyledText, - SPACING, RadioButton, + SPACING, truncateString, TYPOGRAPHY, } from '@opentrons/components' diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx index a80ea2b1f84..ec4df679049 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupParameters/ProtocolSetupParameters.tsx @@ -321,7 +321,6 @@ export function ProtocolSetupParameters({ ) - // ToDo (kk:06/18/2024) ff will be removed when we freeze the code if (chooseCsvFileScreen != null) { children = ( - {detail} + {title === 'CSV File' && detail != null + ? truncateString(detail, CSV_FILE_MAX_LENGTH) + : detail} {subDetail != null && detail != null ?
: null} {subDetail} diff --git a/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx index 17ef7de2c3c..e522fb1dae7 100644 --- a/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/ODD/RobotDashboard/RecentRunProtocolCard.tsx @@ -160,7 +160,8 @@ export function ProtocolWithLastRun({ } // TODO(BC, 2023-06-05): see if addSuffix false allow can remove usage of .replace here const formattedLastRunTime = formatDistance( - new Date(runData.createdAt), + // Fallback to current date if completedAt is null, though this should never happen since runs must be completed to appear in dashboard + new Date(runData.completedAt ?? new Date()), new Date(), { addSuffix: true, diff --git a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index e03f548f069..10ee119176e 100644 --- a/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/ODD/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -87,7 +87,7 @@ const missingBoth = [ const mockRunData = { id: RUN_ID, createdAt: '2022-05-03T21:36:12.494778+00:00', - completedAt: 'thistime', + completedAt: '2023-05-03T21:36:12.494778+00:00', startedAt: 'thistime', protocolId: 'mockProtocolId', status: RUN_STATUS_FAILED, @@ -169,7 +169,7 @@ describe('RecentRunProtocolCard', () => { it('should render text', () => { render(props) const lastRunTime = formatDistance( - new Date(mockRunData.createdAt), + new Date(mockRunData.completedAt), new Date(), { addSuffix: true, diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 39022810b56..ed11df4352d 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -113,6 +113,7 @@ export const PipetteWizardFlows = ( const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = useState< string | null >(null) + const [errorMessage, setShowErrorMessage] = useState(null) // we should start checking for run deletion only after the maintenance run is created // and the useCurrentRun poll has returned that created id const [ @@ -143,6 +144,9 @@ export const PipetteWizardFlows = ( onSuccess: response => { setCreatedMaintenanceRunId(response.data.id) }, + onError: error => { + setShowErrorMessage(error.message) + }, }, host ) @@ -169,7 +173,6 @@ export const PipetteWizardFlows = ( closeFlow, ]) - const [errorMessage, setShowErrorMessage] = useState(null) const [isExiting, setIsExiting] = useState(false) const proceed = (): void => { if (!isCommandMutationLoading) { @@ -281,9 +284,11 @@ export const PipetteWizardFlows = ( let onExit if (currentStep == null) return null let modalContent: JSX.Element =
UNASSIGNED STEP
+ // These flows often have custom error messaging, so this fallback modal is shown only in specific circumstances. if ( (isExiting && errorMessage != null) || - maintenanceRunData?.data.status === RUN_STATUS_FAILED + maintenanceRunData?.data.status === RUN_STATUS_FAILED || + (errorMessage != null && createdMaintenanceRunId == null) ) { modalContent = ( (false) + const [showIcon, setShowIcon] = useState(false) const [ showFailedAnalysisModal, setShowFailedAnalysisModal, - ] = React.useState(false) + ] = useState(false) const { t, i18n } = useTranslation(['protocol_info', 'branded']) const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const longpress = useLongPress() const queryClient = useQueryClient() const host = useHost() + const updatedLastRun = useUpdatedLastRunTime(lastRun) const { id: protocolId, analysisSummaries } = protocol const { @@ -121,7 +122,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) setTargetProtocolId(protocol.id) @@ -195,13 +196,8 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { if (isFailedAnalysis) protocolCardBackgroundColor = COLORS.red35 if (isRequiredCSV) protocolCardBackgroundColor = COLORS.yellow35 - const textWrap = (lastRun?: string): string => { - if (lastRun != null) { - lastRun = formatDistance(new Date(lastRun), new Date(), { - addSuffix: true, - }).replace('about ', '') - } - return lastRun === 'less than a minute ago' ? 'normal' : 'nowrap' + const textWrap = (updatedLastRun: string): string => { + return updatedLastRun === 'less than a minute ago' ? 'normal' : 'nowrap' } return ( @@ -257,13 +253,9 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element { - {lastRun != null - ? formatDistance(new Date(lastRun), new Date(), { - addSuffix: true, - }).replace('about ', '') - : t('no_history')} + {updatedLastRun} diff --git a/app/src/pages/ODD/ProtocolDashboard/hooks.ts b/app/src/pages/ODD/ProtocolDashboard/hooks.ts new file mode 100644 index 00000000000..6db041786ff --- /dev/null +++ b/app/src/pages/ODD/ProtocolDashboard/hooks.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' +import { formatDistance } from 'date-fns' +import { useTranslation } from 'react-i18next' + +import type { TFunction } from 'i18next' + +const UPDATE_TIME_INTERVAL_MS = 60000 + +// Given the last run timestamp, update the time since the last run on an interval. +export function useUpdatedLastRunTime(lastRun: string | undefined): string { + const { t } = useTranslation(['protocol_info']) + + const [updatedLastRun, setUpdatedLastRun] = useState(() => + computeLastRunFromNow(lastRun, t as TFunction) + ) + useEffect(() => { + const timer = setInterval(() => { + setUpdatedLastRun(computeLastRunFromNow(lastRun, t as TFunction)) + }, UPDATE_TIME_INTERVAL_MS) + + return () => { + clearInterval(timer) + } + }, [lastRun, t]) + + return updatedLastRun +} + +function computeLastRunFromNow( + lastRun: string | undefined, + t: TFunction +): string { + return lastRun != null + ? formatDistance(new Date(lastRun), new Date(), { + addSuffix: true, + }).replace('about ', '') + : t('no_history') +} diff --git a/app/src/pages/ODD/ProtocolDashboard/index.tsx b/app/src/pages/ODD/ProtocolDashboard/index.tsx index de775795ded..cddc9ee0a1f 100644 --- a/app/src/pages/ODD/ProtocolDashboard/index.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/index.tsx @@ -252,9 +252,10 @@ export function ProtocolDashboard(): JSX.Element { {sortedProtocols.map(protocol => { - const lastRun = runs.data?.data.find( + // Run data is ordered based on timestamp. We want the last time a matching run was ran. + const lastRun = runs.data?.data.findLast( run => run.protocolId === protocol.id - )?.createdAt + )?.completedAt return ( { - if (isRunCurrent && enteredER === false) { + if ( + isRunCurrent && + runSummaryNoFixit != null && + !lastRunCommandPromptedErrorRecovery(runSummaryNoFixit) + ) { + console.log('HITTING THIS') void determineTipStatus() } - }, [isRunCurrent, enteredER]) + }, [runSummaryNoFixit, isRunCurrent]) const returnToQuickTransfer = (): void => { closeCurrentRunIfValid(() => { diff --git a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts index 481a3622f05..31863f238e6 100644 --- a/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts +++ b/app/src/redux-resources/runs/hooks/useRequiredSetupStepsInOrder.ts @@ -56,14 +56,19 @@ const keysInOrder = ( protocolAnalysis == null ? NO_ANALYSIS_STEPS_IN_ORDER : ALL_STEPS_IN_ORDER.filter((stepKey: StepKey) => { - if (protocolAnalysis.modules.length === 0) { - return stepKey !== MODULE_SETUP_STEP_KEY + if ( + stepKey === MODULE_SETUP_STEP_KEY && + protocolAnalysis.modules.length === 0 + ) { + return false + } else if ( + stepKey === LIQUID_SETUP_STEP_KEY && + protocolAnalysis.liquids.length === 0 + ) { + return false + } else { + return true } - - if (protocolAnalysis.liquids.length === 0) { - return stepKey !== LIQUID_SETUP_STEP_KEY - } - return true }) return { orderedSteps: orderedSteps as StepKey[], orderedApplicableSteps } } diff --git a/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts b/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts new file mode 100644 index 00000000000..6d0de5c6d05 --- /dev/null +++ b/app/src/resources/instruments/__tests__/useTipAttachmentStatus.test.ts @@ -0,0 +1,277 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { act, renderHook, waitFor } from '@testing-library/react' + +import { + getCommands, + getInstruments, + getRunCurrentState, +} from '@opentrons/api-client' +import { getPipetteModelSpecs } from '@opentrons/shared-data' +import { useHost } from '@opentrons/react-api-client' + +import { mockPipetteInfo } from '/app/redux/pipettes/__fixtures__' +import { useTipAttachmentStatus } from '../useTipAttachmentStatus' + +import type { PipetteModelSpecs } from '@opentrons/shared-data' +import type { PipetteData } from '@opentrons/api-client' + +vi.mock('@opentrons/shared-data', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + getPipetteModelSpecs: vi.fn(), + } +}) +vi.mock('@opentrons/api-client') +vi.mock('@opentrons/react-api-client') + +const MOCK_HOST = { ip: '1.2.3.4', port: 31950 } as any +const MOCK_RUN_ID = 'run-123' + +const MOCK_ACTUAL_PIPETTE = { + ...mockPipetteInfo.pipetteSpecs, + model: 'model', + tipLength: { + value: 20, + }, +} as PipetteModelSpecs + +const mockPipetteData: PipetteData = { + mount: 'left', + instrumentType: 'pipette', + instrumentModel: 'p1000_single_v3.6', + ok: true, +} as any + +const mockSecondPipetteData: PipetteData = { + ...mockPipetteData, + mount: 'right', +} + +const mockRunRecord = { + data: { + pipettes: [ + { id: 'pipette-1', mount: 'left' }, + { id: 'pipette-2', mount: 'right' }, + ], + }, +} as any + +const mockTipStates = { + 'pipette-1': { hasTip: true }, + 'pipette-2': { hasTip: true }, +} + +describe('useTipAttachmentStatus', () => { + beforeEach(() => { + vi.mocked(useHost).mockReturnValue(MOCK_HOST) + vi.mocked(getPipetteModelSpecs).mockReturnValue(MOCK_ACTUAL_PIPETTE) + + vi.mocked(getInstruments).mockResolvedValue({ + data: { data: [mockPipetteData, mockSecondPipetteData] }, + } as any) + + vi.mocked(getRunCurrentState).mockResolvedValue({ + data: { data: { tipStates: mockTipStates } }, + } as any) + + vi.mocked(getCommands).mockResolvedValue({ + data: { data: [{ commandType: 'mockType' }] }, + } as any) + }) + + const renderTipAttachmentStatus = () => { + return renderHook(() => + useTipAttachmentStatus({ + runId: MOCK_RUN_ID, + runRecord: mockRunRecord, + }) + ) + } + + it('should return the correct initial state', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toEqual(null) + expect(result.current.initialPipettesWithTipsCount).toEqual(null) + }) + + it('should determine tip status and update state accordingly', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(true) + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + expect(result.current.initialPipettesWithTipsCount).toBe(2) + }) + + it('should handle network errors', async () => { + vi.mocked(getInstruments).mockRejectedValueOnce(new Error('Error')) + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toBeNull() + }) + + it('should reset tip status', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + act(() => { + result.current.resetTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(false) + expect(result.current.aPipetteWithTip).toEqual(null) + expect(result.current.initialPipettesWithTipsCount).toEqual(null) + }) + + it('should set tip status resolved and a state', async () => { + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved() + }) + + await waitFor(() => + expect(result.current.aPipetteWithTip?.mount).toBe('right') + ) + }) + + it('should call onEmptyCache callback when cache becomes empty', async () => { + vi.mocked(getRunCurrentState).mockResolvedValueOnce({ + data: { + data: { + tipStates: { + 'pipette-1': { hasTip: true }, + 'pipette-2': { hasTip: false }, + }, + }, + }, + } as any) + + const onEmptyCacheMock = vi.fn() + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved(onEmptyCacheMock) + }) + + await waitFor(() => { + expect(onEmptyCacheMock).toHaveBeenCalled() + }) + }) + + it('should handle tipPhysicallyMissing error by assuming tip is attached', async () => { + vi.mocked(getCommands).mockResolvedValueOnce({ + data: { + data: [ + { + error: { + errorType: 'tipPhysicallyMissing', + }, + }, + ], + }, + } as any) + + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.areTipsAttached).toBe(true) + }) + + it('should call onTipsDetected callback when tips remain after resolution', async () => { + const onTipsDetectedMock = vi.fn() + const { result } = renderTipAttachmentStatus() + + await waitFor(() => { + expect(result.current).toBeDefined() + }) + + await act(async () => { + await result.current.determineTipStatus() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'left', + specs: MOCK_ACTUAL_PIPETTE, + }) + + act(() => { + result.current.setTipStatusResolved(undefined, onTipsDetectedMock) + }) + + await waitFor(() => { + expect(onTipsDetectedMock).toHaveBeenCalled() + }) + + expect(result.current.aPipetteWithTip).toMatchObject({ + mount: 'right', + specs: MOCK_ACTUAL_PIPETTE, + }) + }) +}) diff --git a/app/src/resources/instruments/index.ts b/app/src/resources/instruments/index.ts index 16fae1ecad8..d88a2c7215f 100644 --- a/app/src/resources/instruments/index.ts +++ b/app/src/resources/instruments/index.ts @@ -1,3 +1,4 @@ export * from './useAttachedPipettes' export * from './useAttachedPipetteCalibrations' export * from './useAttachedPipettesFromInstrumentsQuery' +export * from './useTipAttachmentStatus' diff --git a/app/src/resources/instruments/useTipAttachmentStatus.ts b/app/src/resources/instruments/useTipAttachmentStatus.ts new file mode 100644 index 00000000000..ee0d6449ea6 --- /dev/null +++ b/app/src/resources/instruments/useTipAttachmentStatus.ts @@ -0,0 +1,235 @@ +import { useState, useCallback } from 'react' +import head from 'lodash/head' + +import { useHost } from '@opentrons/react-api-client' +import { + getCommands, + getInstruments, + getRunCurrentState, +} from '@opentrons/api-client' +import { getPipetteModelSpecs } from '@opentrons/shared-data' + +import type { + HostConfig, + Mount, + PipetteData, + Run, + RunCommandSummary, +} from '@opentrons/api-client' +import type { PipetteModelSpecs } from '@opentrons/shared-data' + +export interface PipetteWithTip { + mount: Mount + specs: PipetteModelSpecs +} + +export interface PipetteTipState { + specs: PipetteModelSpecs | null + mount: Mount + hasTip: boolean +} + +export interface TipAttachmentStatusParams { + runId: string + runRecord: Run | null +} + +export interface TipAttachmentStatusResult { + /** Updates the pipettes with tip cache. Determine whether tips are likely attached on one or more pipettes, assuming + * tips are attached when there's uncertainty. + * + * NOTE: This function makes a few network requests on each invocation! + * */ + determineTipStatus: () => Promise + /* Whether tips are likely attached on *any* pipette. Typically called after determineTipStatus() */ + areTipsAttached: boolean + /* Resets the cached pipettes with tip statuses to null. */ + resetTipStatus: () => void + /** Removes the first element from the tip attached cache if present. + * @param {Function} onEmptyCache After removing the pipette from the cache, if the attached tip cache is empty, invoke this callback. + * @param {Function} onTipsDetected After removing the pipette from the cache, if the attached tip cache is not empty, invoke this callback. + * */ + setTipStatusResolved: ( + onEmptyCache?: () => void, + onTipsDetected?: () => void + ) => Promise + /* Relevant pipette information for a pipette with a tip attached. If both pipettes have tips attached, return the left pipette. */ + aPipetteWithTip: PipetteWithTip | null + /* The initial number of pipettes with tips. Null if there has been no tip check yet. */ + initialPipettesWithTipsCount: number | null +} + +// Returns various utilities for interacting with the cache of pipettes with tips attached. +export function useTipAttachmentStatus( + params: TipAttachmentStatusParams +): TipAttachmentStatusResult { + const { runId, runRecord } = params + const host = useHost() + const [pipettesWithTip, setPipettesWithTip] = useState([]) + const [initialPipettesCount, setInitialPipettesCount] = useState< + number | null + >(null) + + const aPipetteWithTip = head(pipettesWithTip) ?? null + const areTipsAttached = + pipettesWithTip.length > 0 && head(pipettesWithTip)?.specs != null + + const determineTipStatus = useCallback((): Promise => { + return Promise.all([ + getInstruments(host as HostConfig), + getRunCurrentState(host as HostConfig, runId), + getCommands(host as HostConfig, runId, { + includeFixitCommands: false, + pageLength: 1, + cursor: null, + }), + ]) + .then(([attachedInstruments, currentState, commandsData]) => { + const { tipStates } = currentState.data.data + + const pipetteInfo = validatePipetteInfo( + attachedInstruments?.data.data as PipetteData[] + ) + + const pipetteInfoById = createPipetteInfoById(runRecord, pipetteInfo) + const pipettesWithTipsData = getPipettesWithTipsData( + // eslint-disable-next-line + tipStates, + pipetteInfoById, + commandsData.data.data as RunCommandSummary[] + ) + const pipettesWithTipAndSpecs = filterPipettesWithTips( + pipettesWithTipsData + ) + + setPipettesWithTip(pipettesWithTipAndSpecs) + + if (initialPipettesCount === null) { + setInitialPipettesCount(pipettesWithTipAndSpecs.length) + } + + return Promise.resolve(pipettesWithTipAndSpecs) + }) + .catch(e => { + console.error(`Error during tip status check: ${e.message}`) + return Promise.resolve([]) + }) + }, [host, initialPipettesCount, runId, runRecord]) + + const resetTipStatus = (): void => { + setPipettesWithTip([]) + setInitialPipettesCount(null) + } + + const setTipStatusResolved = ( + onEmptyCache?: () => void, + onTipsDetected?: () => void + ): Promise => { + return new Promise(resolve => { + setPipettesWithTip(prevPipettesWithTip => { + const newState = [...prevPipettesWithTip.slice(1)] + if (newState.length === 0) { + onEmptyCache?.() + } else { + onTipsDetected?.() + } + + resolve(newState[0]) + return newState + }) + }) + } + + return { + areTipsAttached, + determineTipStatus, + resetTipStatus, + aPipetteWithTip, + setTipStatusResolved, + initialPipettesWithTipsCount: initialPipettesCount, + } +} + +// Return good pipettes from instrument data. +const validatePipetteInfo = ( + attachedInstruments: PipetteData[] | null +): PipetteData[] => { + const goodPipetteInfo = + attachedInstruments?.filter( + instr => instr.instrumentType === 'pipette' && instr.ok + ) ?? null + + if (goodPipetteInfo == null) { + throw new Error( + 'Attached instrument pipettes differ from current state pipettes.' + ) + } + + return goodPipetteInfo +} + +// Associate pipette info with a pipette id. +const createPipetteInfoById = ( + runRecord: Run | null, + pipetteInfo: PipetteData[] +): Record => { + const pipetteInfoById: Record = {} + + runRecord?.data.pipettes.forEach(p => { + const pipetteInfoForThisPipette = pipetteInfo.find( + goodPipette => p.mount === goodPipette.mount + ) + if (pipetteInfoForThisPipette != null) { + pipetteInfoById[p.id] = pipetteInfoForThisPipette + } + }) + + return pipetteInfoById +} + +const getPipettesWithTipsData = ( + tipStates: Record, + pipetteInfoById: Record, + commands: RunCommandSummary[] +): PipetteTipState[] => { + return Object.entries(tipStates).map(([pipetteId, tipInfo]) => { + const pipetteInfo = pipetteInfoById[pipetteId] + const specs = getPipetteModelSpecs(pipetteInfo.instrumentModel) + return { + specs, + mount: pipetteInfo.mount, + hasTip: getMightHaveTipGivenCommands(Boolean(tipInfo.hasTip), commands), + } + }) +} + +const PICK_UP_TIP_COMMAND_TYPES: Array = [ + 'pickUpTip', +] as const + +// Sometimes, the robot and the tip status util have different ideas of when tips are attached. +// For example, if a pickUpTip command fails, the robot does not think a tip is attached. However, we want to be +// conservative and prompt drop tip wizard in case there are tips attached unexpectedly. +const getMightHaveTipGivenCommands = ( + hasTip: boolean, + commands: RunCommandSummary[] +): boolean => { + const lastRunProtocolCommand = commands[commands.length - 1] + + if ( + PICK_UP_TIP_COMMAND_TYPES.includes(lastRunProtocolCommand.commandType) || + lastRunProtocolCommand?.error?.errorType === 'tipPhysicallyMissing' + ) { + return true + } else { + return hasTip + } +} + +const filterPipettesWithTips = ( + pipettesWithTipsData: PipetteTipState[] +): PipetteWithTip[] => { + return pipettesWithTipsData.filter( + pipette => pipette.specs != null && pipette.hasTip + ) as PipetteWithTip[] +} diff --git a/app/src/transformations/analysis/getProtocolModulesInfo.ts b/app/src/transformations/analysis/getProtocolModulesInfo.ts index ee1da1a2392..8a268c2694b 100644 --- a/app/src/transformations/analysis/getProtocolModulesInfo.ts +++ b/app/src/transformations/analysis/getProtocolModulesInfo.ts @@ -3,7 +3,6 @@ import { getModuleDef2, getLoadedLabwareDefinitionsByUri, getPositionFromSlotId, - NON_USER_ADDRESSABLE_LABWARE, } from '@opentrons/shared-data' import { getModuleInitialLoadInfo } from '../commands' import type { @@ -39,8 +38,7 @@ export const getProtocolModulesInfo = ( protocolData.commands .filter( (command): command is LoadLabwareRunTimeCommand => - command.commandType === 'loadLabware' && - !NON_USER_ADDRESSABLE_LABWARE.includes(command.params.loadName) + command.commandType === 'loadLabware' ) .find( (command: LoadLabwareRunTimeCommand) => diff --git a/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts b/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts index 30c79281649..ebc31e37ccc 100644 --- a/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts +++ b/app/src/transformations/commands/transformations/getLabwareSetupItemGroups.ts @@ -1,8 +1,5 @@ import partition from 'lodash/partition' -import { - getLabwareDisplayName, - NON_USER_ADDRESSABLE_LABWARE, -} from '@opentrons/shared-data' +import { getLabwareDisplayName } from '@opentrons/shared-data' import type { LabwareDefinition2, @@ -46,8 +43,7 @@ export function getLabwareSetupItemGroups( commands.reduce((acc, c) => { if ( c.commandType === 'loadLabware' && - c.result?.definition?.metadata?.displayCategory !== 'trash' && - !NON_USER_ADDRESSABLE_LABWARE.includes(c.params?.loadName) + c.result?.definition?.metadata?.displayCategory !== 'trash' ) { const { location, displayName } = c.params const { definition } = c.result ?? {} diff --git a/components/src/atoms/Checkbox/index.tsx b/components/src/atoms/Checkbox/index.tsx index 8ace61cb0bf..44c2ba8ee04 100644 --- a/components/src/atoms/Checkbox/index.tsx +++ b/components/src/atoms/Checkbox/index.tsx @@ -48,7 +48,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { align-items: ${ALIGN_CENTER}; flex-direction: ${DIRECTION_ROW}; color: ${isChecked ? COLORS.white : COLORS.black90}; - background-color: ${isChecked ? COLORS.blue50 : COLORS.blue35}; + background-color: ${isChecked ? COLORS.blue50 : COLORS.blue30}; border-radius: ${type === 'round' ? BORDERS.borderRadiusFull : BORDERS.borderRadius8}; @@ -68,6 +68,9 @@ export function Checkbox(props: CheckboxProps): JSX.Element { background-color: ${COLORS.grey35}; color: ${COLORS.grey50}; } + &:hover { + background-color: ${isChecked ? COLORS.blue55 : COLORS.blue35}; + } @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing20}; diff --git a/components/src/atoms/InputField/index.tsx b/components/src/atoms/InputField/index.tsx index 59b404b476f..06b4ad68533 100644 --- a/components/src/atoms/InputField/index.tsx +++ b/components/src/atoms/InputField/index.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, DIRECTION_COLUMN, DIRECTION_ROW, + NO_WRAP, TEXT_ALIGN_RIGHT, } from '../../styles' import { BORDERS, COLORS } from '../../helix-design-system' @@ -251,6 +252,7 @@ export const InputField = React.forwardRef( font-weight: ${TYPOGRAPHY.fontWeightRegular}; line-height: ${TYPOGRAPHY.lineHeight28}; justify-content: ${textAlign}; + white-space: ${NO_WRAP}; } ` diff --git a/components/src/atoms/Tag/index.tsx b/components/src/atoms/Tag/index.tsx index c41025dd25b..74c72da486e 100644 --- a/components/src/atoms/Tag/index.tsx +++ b/components/src/atoms/Tag/index.tsx @@ -1,7 +1,7 @@ import { css } from 'styled-components' import { BORDERS, COLORS } from '../../helix-design-system' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_ROW } from '../../styles' +import { ALIGN_CENTER, DIRECTION_ROW, FLEX_MAX_CONTENT } from '../../styles' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { Icon } from '../../icons' import { LegacyStyledText } from '../StyledText' @@ -19,6 +19,7 @@ export interface TagProps { iconPosition?: 'left' | 'right' /** Tagicon */ iconName?: IconName + shrinkToContent?: boolean } const defaultColors = { @@ -42,11 +43,12 @@ const TAG_PROPS_BY_TYPE: Record< } export function Tag(props: TagProps): JSX.Element { - const { iconName, type, text, iconPosition } = props + const { iconName, type, text, iconPosition, shrinkToContent = false } = props const DEFAULT_CONTAINER_STYLE = css` padding: ${SPACING.spacing2} ${SPACING.spacing8}; border-radius: ${BORDERS.borderRadius4}; + width: ${shrinkToContent ? FLEX_MAX_CONTENT : 'none'}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing12}; diff --git a/components/src/atoms/buttons/EmptySelectorButton.tsx b/components/src/atoms/buttons/EmptySelectorButton.tsx index 42e8822fc35..da34a8ba710 100644 --- a/components/src/atoms/buttons/EmptySelectorButton.tsx +++ b/components/src/atoms/buttons/EmptySelectorButton.tsx @@ -1,27 +1,18 @@ import styled from 'styled-components' import { Flex } from '../../primitives' import { + ALIGN_CENTER, CURSOR_DEFAULT, CURSOR_POINTER, + FLEX_MAX_CONTENT, Icon, - SPACING, - StyledText, JUSTIFY_CENTER, JUSTIFY_START, - ALIGN_CENTER, - FLEX_MAX_CONTENT, + SPACING, + StyledText, } from '../../index' -import { - black90, - blue30, - blue50, - grey30, - grey40, - white, -} from '../../helix-design-system/colors' -import { borderRadius8 } from '../../helix-design-system/borders' +import { BORDERS, COLORS } from '../../helix-design-system' import type { IconName } from '../../index' - interface EmptySelectorButtonProps { onClick: () => void text: string @@ -41,10 +32,9 @@ export function EmptySelectorButton( ` border: none; width: ${FLEX_MAX_CONTENT}; height: ${FLEX_MAX_CONTENT}; - cursor: ${({ disabled }) => (disabled ? CURSOR_DEFAULT : CURSOR_POINTER)}; + cursor: ${CURSOR_POINTER}; + background-color: ${COLORS.blue30}; + border-radius: ${BORDERS.borderRadius8}; + &:focus-visible { - outline: 2px solid ${white}; - box-shadow: 0 0 0 4px ${blue50}; - border-radius: ${borderRadius8}; + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + &:hover { + background-color: ${COLORS.blue35}; + } + &:disabled { + background-color: ${COLORS.grey20}; + cursor: ${CURSOR_DEFAULT}; } ` diff --git a/components/src/atoms/buttons/RadioButton.tsx b/components/src/atoms/buttons/RadioButton.tsx index f960987f67f..a4d1a769144 100644 --- a/components/src/atoms/buttons/RadioButton.tsx +++ b/components/src/atoms/buttons/RadioButton.tsx @@ -212,5 +212,6 @@ const SettingButtonLabel = styled.label` -webkit-box-orient: ${({ maxLines }) => maxLines != null ? 'vertical' : 'none'}; word-wrap: break-word; + word-break: break-all; } ` diff --git a/components/src/hardware-sim/ProtocolDeck/utils/getModulesInSlots.ts b/components/src/hardware-sim/ProtocolDeck/utils/getModulesInSlots.ts index 612759b3d01..0d9da4b48bd 100644 --- a/components/src/hardware-sim/ProtocolDeck/utils/getModulesInSlots.ts +++ b/components/src/hardware-sim/ProtocolDeck/utils/getModulesInSlots.ts @@ -2,7 +2,6 @@ import { SPAN7_8_10_11_SLOT, getModuleDef2, getLoadedLabwareDefinitionsByUri, - NON_USER_ADDRESSABLE_LABWARE, } from '@opentrons/shared-data' import type { CompletedProtocolAnalysis, @@ -37,8 +36,7 @@ export const getModulesInSlots = ( commands .filter( (command): command is LoadLabwareRunTimeCommand => - command.commandType === 'loadLabware' && - !NON_USER_ADDRESSABLE_LABWARE.includes(command.params.loadName) + command.commandType === 'loadLabware' ) .find( (command: LoadLabwareRunTimeCommand) => diff --git a/components/src/organisms/Toolbox/index.tsx b/components/src/organisms/Toolbox/index.tsx index de1748601c8..147b8b0eda2 100644 --- a/components/src/organisms/Toolbox/index.tsx +++ b/components/src/organisms/Toolbox/index.tsx @@ -140,7 +140,6 @@ export function Toolbox(props: ToolboxProps): JSX.Element { Tuple[CSVReport, FlexStacker]: @@ -42,6 +42,28 @@ async def _main(cfg: TestConfig) -> None: # BUILD REPORT report, stacker = build_stacker_report(cfg.simulate) + if not cfg.simulate: + # Perform initial checks before starting tests + # 1. estop should not be pressed + # 2. platform should be removed + if stacker.get_estop(): + ui.print_error("ESTOP is pressed, please release it before starting") + ui.get_user_ready("Release ESTOP") + if stacker.get_estop(): + ui.print_error("ESTOP is still pressed, cannot start tests") + return + + platform_state = stacker.get_platform_status() + if platform_state is PlatformStatus.ERROR: + ui.print_error("Platform sensors are not working properly, aborting") + return + if platform_state is not PlatformStatus.REMOVED: + ui.print_error("Platform must be removed from the carrier before starting") + ui.get_user_ready("Remove platform from {platform_state.value}") + if stacker.get_platform_status() is not PlatformStatus.REMOVED: + ui.print_error("Platform is still detected, cannot start tests") + return + # RUN TESTS for section, test_run in cfg.tests.items(): ui.print_title(section.value) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py index e8bc37da959..7a339eddde2 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py @@ -7,6 +7,11 @@ from . import ( test_connectivity, + test_z_axis, + test_x_axis, + test_l_axis, + test_door_switch, + test_estop, ) @@ -14,6 +19,11 @@ class TestSection(enum.Enum): """Test Section.""" CONNECTIVITY = "CONNECTIVITY" + Z_AXIS = "Z_AXIS" + L_AXIS = "L_AXIS" + X_AXIS = "X_AXIS" + DOOR_SWITCH = "DOOR_SWITCH" + ESTOP = "ESTOP" @dataclass @@ -29,6 +39,26 @@ class TestConfig: TestSection.CONNECTIVITY, test_connectivity.run, ), + ( + TestSection.Z_AXIS, + test_z_axis.run, + ), + ( + TestSection.X_AXIS, + test_x_axis.run, + ), + ( + TestSection.L_AXIS, + test_l_axis.run, + ), + ( + TestSection.ESTOP, + test_estop.run, + ), + ( + TestSection.DOOR_SWITCH, + test_door_switch.run, + ), ] @@ -40,6 +70,26 @@ def build_report(test_name: str) -> CSVReport: CSVSection( title=TestSection.CONNECTIVITY.value, lines=test_connectivity.build_csv_lines(), - ) + ), + CSVSection( + title=TestSection.Z_AXIS.value, + lines=test_z_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.X_AXIS.value, + lines=test_x_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.L_AXIS.value, + lines=test_l_axis.build_csv_lines(), + ), + CSVSection( + title=TestSection.ESTOP.value, + lines=test_estop.build_csv_lines(), + ), + CSVSection( + title=TestSection.DOOR_SWITCH.value, + lines=test_door_switch.build_csv_lines(), + ), ], ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py index 04d833fa8a5..443140573bd 100644 --- a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py @@ -1,4 +1,5 @@ """FLEX Stacker Driver.""" +from typing import Tuple from dataclasses import dataclass import serial # type: ignore[import] from serial.tools.list_ports import comports # type: ignore[import] @@ -26,6 +27,73 @@ class StackerInfo: sn: str +class StackerAxis(Enum): + """Stacker Axis.""" + + X = "X" + Z = "Z" + L = "L" + + def __str__(self) -> str: + """Name.""" + return self.name + + +class PlatformStatus(Enum): + """Platform Status.""" + + REMOVED = 0 + EXTENTED = 1 + RETRACTED = 2 + ERROR = 4 + + @classmethod + def from_tuple(cls, status: Tuple[int, int]) -> "PlatformStatus": + """Get platform status from tuple.""" + if status == (0, 0): + return PlatformStatus.REMOVED + if status == (1, 0): + return PlatformStatus.EXTENTED + if status == (0, 1): + return PlatformStatus.RETRACTED + return PlatformStatus.ERROR + + +class Direction(Enum): + """Direction.""" + + RETRACT = 0 + EXTENT = 1 + + def __str__(self) -> str: + """Convert to tag for clear logging.""" + return "negative" if self == Direction.RETRACT else "positive" + + def opposite(self) -> "Direction": + """Get opposite direction.""" + return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT + + def distance(self, distance: float) -> float: + """Get signed distance, where retract direction is negative.""" + return distance * -1 if self == Direction.RETRACT else distance + + +@dataclass +class MoveParams: + """Move Parameters.""" + + max_speed: float | None = None + acceleration: float | None = None + max_speed_discont: float | None = None + + def __str__(self) -> str: + """Convert to string.""" + v = "V" + str(self.max_speed) if self.max_speed else "" + a = "A" + str(self.acceleration) if self.acceleration else "" + d = "D" + str(self.max_speed_discont) if self.max_speed_discont else "" + return f"{v} {a} {d}".strip() + + class FlexStacker: """FLEX Stacker Driver.""" @@ -53,7 +121,7 @@ def __init__(self, port: str, simulating: bool = False) -> None: def _send_and_recv(self, msg: str, guard_ret: str = "") -> str: """Internal utility to send a command and receive the response.""" - assert self._simulating + assert not self._simulating self._serial.write(msg.encode()) ret = self._serial.readline() if guard_ret: @@ -87,6 +155,91 @@ def set_serial_number(self, sn: str) -> None: return self._send_and_recv(f"M996 {sn}\n", "M996 OK") + def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: + """Get limit switch status. + + :return: True if limit switch is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M119 .*{axis.name}{direction.name[0]}:(\d).* OK\n") + res = self._send_and_recv("M119\n", "M119 XE:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for limit switch: {res}" + return bool(int(match.group(1))) + + def get_platform_sensor(self, direction: Direction) -> bool: + """Get platform sensor status. + + :return: True if platform is present, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(rf"^M121 .*{direction.name[0]}:(\d).* OK\n") + res = self._send_and_recv("M121\n", "M121 E:") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for platform sensor: {res}" + return bool(int(match.group(1))) + + def get_platform_status(self) -> PlatformStatus: + """Get platform status.""" + if self._simulating: + return PlatformStatus.REMOVED + + _LS_RE = re.compile(r"^M121 E:(\d) R:(\d) OK\n") + res = self._send_and_recv("M121\n", "M121 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for platform status: {res}" + return PlatformStatus.from_tuple((int(match.group(1)), int(match.group(2)))) + + def get_hopper_door_closed(self) -> bool: + """Get whether or not door is closed. + + :return: True if door is closed, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M122 (\d) OK\n") + res = self._send_and_recv("M122\n", "M122 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for hopper door switch: {res}" + return bool(int(match.group(1))) + + def get_estop(self) -> bool: + """Get E-Stop status. + + :return: True if E-Stop is triggered, False otherwise + """ + if self._simulating: + return True + + _LS_RE = re.compile(r"^M112 (\d) OK\n") + res = self._send_and_recv("M112\n", "M112 ") + match = _LS_RE.match(res) + assert match, f"Incorrect Response for E-Stop switch: {res}" + return bool(int(match.group(1))) + + def move_in_mm( + self, axis: StackerAxis, distance: float, params: MoveParams | None = None + ) -> None: + """Move axis.""" + if self._simulating: + return + self._send_and_recv(f"G0 {axis.name}{distance} {params or ''}\n", "G0 OK") + + def move_to_limit_switch( + self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None + ) -> None: + """Move until limit switch is triggered.""" + if self._simulating: + return + self._send_and_recv( + f"G5 {axis.name}{direction.value} {params or ''}\n", "G5 OK" + ) + def __del__(self) -> None: """Close serial port.""" if not self._simulating: diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py new file mode 100644 index 00000000000..ab104a10d01 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_door_switch.py @@ -0,0 +1,36 @@ +"""Test Door Switch.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("close-door", [CSVResult]), + CSVLine("open-door", [CSVResult]), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + ui.print_header("Close Door") + if not driver._simulating: + ui.get_user_ready("Close the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "close-door", [CSVResult.from_bool(closed)]) + + ui.print_header("Open Door") + if not driver._simulating: + ui.get_user_ready("Open the hopper door") + closed = driver.get_hopper_door_closed() + report(section, "open-door", [CSVResult.from_bool(not closed)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py new file mode 100644 index 00000000000..2a2f24161b7 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_estop.py @@ -0,0 +1,88 @@ +"""Test E-Stop.""" + + +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, Direction, StackerAxis + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-estop", [CSVResult]), + CSVLine("x-move-disabled", [CSVResult]), + CSVLine("z-move-disabled", [CSVResult]), + CSVLine("l-move-disabled", [CSVResult]), + CSVLine("untrigger-estop", [CSVResult]), + ] + + +def axis_at_limit(driver: FlexStacker, axis: StackerAxis) -> Direction: + """Check which direction an axis is at the limit switch.""" + if axis is StackerAxis.L: + # L axis only has one limit switch + if driver.get_limit_switch(axis, Direction.RETRACT): + print(axis, "is at ", Direction.RETRACT, "limit switch") + return Direction.RETRACT + else: + for dir in Direction: + if driver.get_limit_switch(axis, dir): + print(axis, "is at ", dir, "limit switch") + return dir + raise RuntimeError(f"{axis} is not at any limit switch") + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + x_limit = axis_at_limit(driver, StackerAxis.X) + z_limit = axis_at_limit(driver, StackerAxis.Z) + l_limit = axis_at_limit(driver, StackerAxis.L) + + ui.print_header("Trigger E-Stop") + if not driver._simulating: + ui.get_user_ready("Trigger the E-Stop") + + if not driver.get_estop(): + print("E-Stop is not triggered") + report(section, "trigger-estop", [CSVResult.FAIL]) + return + + report(section, "trigger-estop", [CSVResult.PASS]) + + print("try to move X axis...") + driver.move_in_mm(StackerAxis.X, x_limit.opposite().distance(10)) + print("X should not move") + report( + section, + "x-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.X, x_limit))], + ) + + print("try to move Z axis...") + driver.move_in_mm(StackerAxis.Z, z_limit.opposite().distance(10)) + print("Z should not move") + report( + section, + "z-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.Z, z_limit))], + ) + + print("try to move L axis...") + driver.move_in_mm(StackerAxis.L, l_limit.opposite().distance(10)) + print("L should not move") + report( + section, + "l-move-disabled", + [CSVResult.from_bool(driver.get_limit_switch(StackerAxis.L, l_limit))], + ) + + if not driver._simulating: + ui.get_user_ready("Untrigger the E-Stop") + report(section, "untrigger-estop", [CSVResult.from_bool(not driver.get_estop())]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py new file mode 100644 index 00000000000..4b3856e92e6 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_l_axis.py @@ -0,0 +1,64 @@ +"""Test L Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine("trigger-latch-switch", [CSVResult]), + CSVLine("release/open-latch", [CSVResult]), + CSVLine("hold/close-latch", [CSVResult]), + ] + + +def get_latch_held_switch(driver: FlexStacker) -> bool: + """Get limit switch.""" + held_switch = driver.get_limit_switch(StackerAxis.L, Direction.RETRACT) + print("(Held Switch triggered) : ", held_switch) + return held_switch + + +def close_latch(driver: FlexStacker) -> None: + """Close latch.""" + driver.move_to_limit_switch(StackerAxis.L, Direction.RETRACT) + + +def open_latch(driver: FlexStacker) -> None: + """Open latch.""" + driver.move_in_mm(StackerAxis.L, 22) + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not get_latch_held_switch(driver): + print("Switch is not triggered, try to trigger it by closing latch...") + close_latch(driver) + if not get_latch_held_switch(driver): + print("!!! Held switch is still not triggered !!!") + report(section, "trigger-latch-switch", [CSVResult.FAIL]) + return + + report(section, "trigger-latch-switch", [CSVResult.PASS]) + + ui.print_header("Latch Release/Open") + open_latch(driver) + success = not get_latch_held_switch(driver) + report(section, "release/open-latch", [CSVResult.from_bool(success)]) + + ui.print_header("Latch Hold/Close") + if not success: + print("Latch must be open to close it") + report(section, "hold/close-latch", [CSVResult.FAIL]) + else: + close_latch(driver) + success = get_latch_held_switch(driver) + report(section, "hold/close-latch", [CSVResult.from_bool(success)]) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py new file mode 100644 index 00000000000..802c12bcae5 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_x_axis.py @@ -0,0 +1,81 @@ +"""Test X Axis.""" +from typing import List, Union +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + CSVLine( + "platform-sensor-trigger-positive-untrigger-negative", + [bool, bool, CSVResult], + ), + CSVLine( + "platform-sensor-trigger-negative-untrigger-positive", + [bool, bool, CSVResult], + ), + ] + + +def test_platform_sensors_for_direction( + driver: FlexStacker, direction: Direction, report: CSVReport, section: str +) -> None: + """Test platform sensors for a given direction.""" + ui.print_header(f"Platform Sensor - {direction} direction") + sensor_result = driver.get_platform_sensor(direction) + opposite_result = not driver.get_platform_sensor(direction.opposite()) + print(f"{direction} sensor triggered: {sensor_result}") + print(f"{direction.opposite()} sensor untriggered: {opposite_result}") + report( + section, + f"platform-sensor-trigger-{direction}-untrigger-{direction.opposite()}", + [ + sensor_result, + opposite_result, + CSVResult.from_bool(sensor_result and opposite_result), + ], + ) + + +def platform_is_removed(driver: FlexStacker) -> bool: + """Check if the platform is removed from the carrier.""" + plus_side = driver.get_platform_sensor(Direction.EXTENT) + minus_side = driver.get_platform_sensor(Direction.RETRACT) + return not plus_side and not minus_side + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + if not driver._simulating and not platform_is_removed(driver): + print("FAILURE - Cannot start tests with platform on the carrier") + return + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.EXTENT, report, section + ) + + if not driver._simulating: + ui.get_user_ready("Place the platform on the X carrier") + + test_platform_sensors_for_direction(driver, Direction.EXTENT, report, section) + + test_limit_switches_per_direction( + driver, StackerAxis.X, Direction.RETRACT, report, section + ) + + test_platform_sensors_for_direction(driver, Direction.RETRACT, report, section) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py new file mode 100644 index 00000000000..58fc733e0dc --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_z_axis.py @@ -0,0 +1,34 @@ +"""Test Z Axis.""" +from typing import List, Union +from hardware_testing.data.csv_report import ( + CSVReport, + CSVLine, + CSVLineRepeating, + CSVResult, +) + +from .utils import test_limit_switches_per_direction +from .driver import FlexStacker, StackerAxis, Direction + + +def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: + """Build CSV Lines.""" + return [ + CSVLine( + "limit-switch-trigger-positive-untrigger-negative", [bool, bool, CSVResult] + ), + CSVLine( + "limit-switch-trigger-negative-untrigger-positive", [bool, bool, CSVResult] + ), + ] + + +def run(driver: FlexStacker, report: CSVReport, section: str) -> None: + """Run.""" + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.EXTENT, report, section + ) + + test_limit_switches_per_direction( + driver, StackerAxis.Z, Direction.RETRACT, report, section + ) diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py new file mode 100644 index 00000000000..2aca90c8886 --- /dev/null +++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/utils.py @@ -0,0 +1,38 @@ +"""Utility functions for the Flex Stacker EVT QC module.""" +from hardware_testing.data import ui +from hardware_testing.data.csv_report import ( + CSVReport, + CSVResult, +) + +from .driver import FlexStacker, StackerAxis, Direction, MoveParams + + +def test_limit_switches_per_direction( + driver: FlexStacker, + axis: StackerAxis, + direction: Direction, + report: CSVReport, + section: str, + speed: float = 50.0, +) -> None: + """Sequence to test the limit switch for one direction.""" + ui.print_header(f"{axis} Limit Switch - {direction} direction") + # first make sure switch is not already triggered by moving in the opposite direction + if driver.get_limit_switch(axis, direction): + print(f"{direction} switch already triggered, moving away...\n") + SAFE_DISTANCE_MM = 10 + driver.move_in_mm(axis, direction.opposite().distance(SAFE_DISTANCE_MM)) + + # move until the limit switch is reached + print(f"moving towards {direction} limit switch...\n") + driver.move_to_limit_switch(axis, direction, MoveParams(max_speed=speed)) + result = driver.get_limit_switch(axis, direction) + opposite_result = not driver.get_limit_switch(axis, direction.opposite()) + print(f"{direction} switch triggered: {result}") + print(f"{direction.opposite()} switch untriggered: {opposite_result}") + report( + section, + f"limit-switch-trigger-{direction}-untrigger-{direction.opposite()}", + [result, opposite_result, CSVResult.from_bool(result and opposite_result)], + ) 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 90637e81540..6be7cc92fab 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 @@ -119,6 +119,7 @@ class TestConfig: num_trials: int droplet_wait_seconds: int simulate: bool + skip_all_pressure: bool @dataclass @@ -700,9 +701,12 @@ async def _test_for_leak( accumulate_raw_data_cb ), "pressure fixture requires recording data to disk" await _move_to_fixture(api, mount) - test_passed = await _fixture_check_pressure( - api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb - ) + if not test_config.skip_all_pressure: + test_passed = await _fixture_check_pressure( + api, mount, test_config, fixture, write_cb, accumulate_raw_data_cb + ) + else: + test_passed = True else: await _pick_up_tip_for_tip_volume(api, mount, tip_volume=tip_volume) await _move_to_reservoir_liquid(api, mount) @@ -1129,7 +1133,9 @@ async def _read_pressure(_sensor_id: SensorId) -> float: return all(results) -async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> bool: +async def _test_diagnostics( + api: OT3API, mount: OT3Mount, write_cb: Callable, cfg: TestConfig +) -> bool: # ENVIRONMENT SENSOR environment_pass = await _test_diagnostics_environment(api, mount, write_cb) print(f"environment: {_bool_to_pass_fail(environment_pass)}") @@ -1146,9 +1152,14 @@ async def _test_diagnostics(api: OT3API, mount: OT3Mount, write_cb: Callable) -> print(f"capacitance: {_bool_to_pass_fail(capacitance_pass)}") write_cb(["diagnostics-capacitance", _bool_to_pass_fail(capacitance_pass)]) # PRESSURE - pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) - print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + if not cfg.skip_all_pressure: + pressure_pass = await _test_diagnostics_pressure(api, mount, write_cb) + print(f"pressure: {_bool_to_pass_fail(pressure_pass)}") + else: + print("Skipping pressure") + pressure_pass = True write_cb(["diagnostics-pressure", _bool_to_pass_fail(pressure_pass)]) + return environment_pass and pressure_pass and encoder_pass and capacitance_pass @@ -1674,7 +1685,9 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 if not test_config.skip_diagnostics: await api.move_to(mount, hover_over_slot_3) await api.move_rel(mount, Point(z=-20)) - test_passed = await _test_diagnostics(api, mount, csv_cb.write) + test_passed = await _test_diagnostics( + api, mount, csv_cb.write, test_config + ) await api.retract(mount) csv_cb.results("diagnostics", test_passed) if not test_config.skip_plunger: @@ -1806,6 +1819,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 arg_parser.add_argument("--skip-plunger", action="store_true") arg_parser.add_argument("--skip-tip-presence", action="store_true") arg_parser.add_argument("--skip-liquid-probe", action="store_true") + arg_parser.add_argument("--skip-all-pressure", action="store_true") arg_parser.add_argument("--fixture-side", choices=["left", "right"], default="left") arg_parser.add_argument("--port", type=str, default="") arg_parser.add_argument("--num-trials", type=int, default=2) @@ -1841,11 +1855,11 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 _cfg = TestConfig( operator_name=operator, skip_liquid=args.skip_liquid, - skip_fixture=args.skip_fixture, + skip_fixture=args.skip_fixture or args.skip_all_pressure, skip_diagnostics=args.skip_diagnostics, skip_plunger=args.skip_plunger, skip_tip_presence=args.skip_tip_presence, - skip_liquid_probe=args.skip_liquid_probe, + skip_liquid_probe=args.skip_liquid_probe or args.skip_all_pressure, fixture_port=args.port, fixture_side=args.fixture_side, fixture_aspirate_sample_count=args.aspirate_sample_count, @@ -1859,6 +1873,7 @@ async def _main(test_config: TestConfig) -> None: # noqa: C901 num_trials=args.num_trials, droplet_wait_seconds=args.wait, simulate=args.simulate, + skip_all_pressure=args.skip_all_pressure, ) # NOTE: overwrite default aspirate sample-count from user's input # FIXME: this value is being set in a few places, maybe there's a way to clean this up diff --git a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py index 41c70ed35a2..710d3c17578 100644 --- a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py +++ b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py @@ -2,6 +2,7 @@ import sys import paramiko as pmk import time +import json import multiprocessing from typing import Optional, List, Any @@ -69,11 +70,10 @@ def run(file_name: str) -> List[Any]: robot_ips = [] robot_names = [] with open(file_name) as file: - for line in file.readlines(): - info = line.split(",") - if "Y" in info[2]: - robot_ips.append(info[0]) - robot_names.append(info[1]) + file_dict = json.load(file) + robot_dict = file_dict.get("ip_address_list") + robot_ips = list(robot_dict.keys()) + robot_names = list(robot_dict.values()) print("Executing Script on All Robots:") # Launch the processes for each robot. processes = [] @@ -89,10 +89,8 @@ def run(file_name: str) -> List[Any]: # Wait for all processes to finish. file_name = sys.argv[1] processes = run(file_name) - for process in processes: process.start() time.sleep(20) - for process in processes: process.join() diff --git a/hardware/opentrons_hardware/firmware_bindings/constants.py b/hardware/opentrons_hardware/firmware_bindings/constants.py index d9dc98def39..77b1dce5b3e 100644 --- a/hardware/opentrons_hardware/firmware_bindings/constants.py +++ b/hardware/opentrons_hardware/firmware_bindings/constants.py @@ -359,6 +359,7 @@ class PipetteName(int, Enum): p1000_96 = 0x04 p50_96 = 0x05 p200_96 = 0x06 + p1000_multi_em = 0x07 unknown = 0xFFFF @@ -462,3 +463,4 @@ class GripperJawState(int, Enum): force_controlling_home = 0x1 force_controlling = 0x2 position_controlling = 0x3 + stopped = 0x4 diff --git a/hardware/opentrons_hardware/hardware_control/motor_position_status.py b/hardware/opentrons_hardware/hardware_control/motor_position_status.py index 90319764922..1ce9bbe3ce5 100644 --- a/hardware/opentrons_hardware/hardware_control/motor_position_status.py +++ b/hardware/opentrons_hardware/hardware_control/motor_position_status.py @@ -152,11 +152,7 @@ def _listener_filter(arbitration_id: ArbitrationId) -> bool: log.warning("Update motor position estimation timed out") raise CommandTimedOutError( "Update motor position estimation timed out", - detail={ - "missing-nodes": ", ".join( - node.name for node in set(nodes).difference(set(data)) - ) - }, + detail={"missing-node": node.name}, ) return data diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index c4a8fc441d0..c18772fe656 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -27,6 +27,7 @@ NAME_LOOKUP: Dict[str, PipetteName] = { "P1KS": PipetteName.p1000_single, "P1KM": PipetteName.p1000_multi, + "P1KP": PipetteName.p1000_multi_em, "P50S": PipetteName.p50_single, "P50M": PipetteName.p50_multi, "P1KH": PipetteName.p1000_96, diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py index 857c0d08f92..64ed76a6856 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_motion_plan.py @@ -2,7 +2,7 @@ import numpy as np from hypothesis import given, assume, strategies as st from hypothesis.extra import numpy as hynp -from typing import Iterator, List, Tuple +from typing import Iterator, List, Tuple, Dict from opentrons_hardware.hardware_control.motion_planning import move_manager from opentrons_hardware.hardware_control.motion_planning.types import ( @@ -210,3 +210,60 @@ def test_close_move_plan( ) assert converged, f"Failed to converge: {blend_log}" + + +def test_pipette_high_speed_motion() -> None: + """Test that updated motion constraint doesn't get overridden by motion planning.""" + origin: Dict[str, int] = { + "X": 499, + "Y": 499, + "Z": 499, + "A": 499, + "B": 499, + "C": 499, + } + target_list = [] + axis_kinds = ["X", "Y", "Z", "A", "B", "C"] + constraints: SystemConstraints[str] = {} + for axis_kind in axis_kinds: + constraints[axis_kind] = AxisConstraints.build( + max_acceleration=500, + max_speed_discont=500, + max_direction_change_speed_discont=500, + max_speed=500, + ) + origin_mapping: Dict[str, float] = {axis_kind: float(origin[axis_kind])} + target_list.append(MoveTarget.build(origin_mapping, 500)) + + set_axis_kind = "A" + dummy_em_pipette_max_speed = 90.0 + manager = move_manager.MoveManager(constraints=constraints) + + new_axis_constraint = AxisConstraints.build( + max_acceleration=float(constraints[set_axis_kind].max_acceleration), + max_speed_discont=float(constraints[set_axis_kind].max_speed_discont), + max_direction_change_speed_discont=float( + constraints[set_axis_kind].max_direction_change_speed_discont + ), + max_speed=90.0, + ) + new_constraints = {} + + for axis_kind in constraints.keys(): + if axis_kind == set_axis_kind: + new_constraints[axis_kind] = new_axis_constraint + else: + new_constraints[axis_kind] = constraints[axis_kind] + + manager.update_constraints(constraints=new_constraints) + converged, blend_log = manager.plan_motion( + origin=origin, + target_list=target_list, + iteration_limit=20, + ) + for move in blend_log[0]: + unit_vector = move.unit_vector + for block in move.blocks: + top_set_axis_speed = unit_vector[set_axis_kind] * block.final_speed + if top_set_axis_speed != 0: + assert abs(top_set_axis_speed) == dummy_em_pipette_max_speed diff --git a/hardware/tests/opentrons_hardware/instruments/test_serials.py b/hardware/tests/opentrons_hardware/instruments/test_serials.py index 7b398eda286..2820b5ffbe5 100644 --- a/hardware/tests/opentrons_hardware/instruments/test_serials.py +++ b/hardware/tests/opentrons_hardware/instruments/test_serials.py @@ -40,6 +40,12 @@ 1, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ), + ( + "P1KPV30", + PipetteName.p1000_multi_em, + 30, + b"\x00" * 16, + ), ], ) def test_scan_valid_pipette_serials( diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index 06dd83061cb..b1510aaf89c 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -182,7 +182,8 @@ export function generateChatPrompt( .join('\n') : `- ${t(values.instruments.pipettes)}` const flexGripper = - values.instruments.flexGripper === FLEX_GRIPPER + values.instruments.flexGripper === FLEX_GRIPPER && + values.instruments.robot === OPENTRONS_FLEX ? `\n- ${t('with_flex_gripper')}` : '' const modules = values.modules diff --git a/opentrons-ai-server/api/domain/anthropic_predict.py b/opentrons-ai-server/api/domain/anthropic_predict.py index abd94b631ba..4ab71c99488 100644 --- a/opentrons-ai-server/api/domain/anthropic_predict.py +++ b/opentrons-ai-server/api/domain/anthropic_predict.py @@ -1,6 +1,6 @@ import uuid from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Literal import requests import structlog @@ -23,7 +23,7 @@ def __init__(self, settings: Settings) -> None: self.model_name: str = settings.anthropic_model_name self.system_prompt: str = SYSTEM_PROMPT self.path_docs: Path = ROOT_PATH / "api" / "storage" / "docs" - self._messages: List[MessageParam] = [ + self.cached_docs: List[MessageParam] = [ { "role": "user", "content": [ @@ -77,19 +77,26 @@ def get_docs(self) -> str: return "\n".join(xml_output) @tracer.wrap() - def generate_message(self, max_tokens: int = 4096) -> Message: + def _process_message( + self, user_id: str, messages: List[MessageParam], message_type: Literal["create", "update"], max_tokens: int = 4096 + ) -> Message: + """ + Internal method to handle message processing with different system prompts. + For now, system prompt is the same. + """ - response = self.client.messages.create( + response: Message = self.client.messages.create( model=self.model_name, system=self.system_prompt, max_tokens=max_tokens, - messages=self._messages, + messages=messages, tools=self.tools, # type: ignore extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"}, + metadata={"user_id": user_id}, ) logger.info( - "Token usage", + f"Token usage: {message_type.capitalize()}", extra={ "input_tokens": response.usage.input_tokens, "output_tokens": response.usage.output_tokens, @@ -100,15 +107,23 @@ def generate_message(self, max_tokens: int = 4096) -> Message: return response @tracer.wrap() - def predict(self, prompt: str) -> str | None: + def process_message( + self, user_id: str, prompt: str, history: List[MessageParam] | None = None, message_type: Literal["create", "update"] = "create" + ) -> str | None: + """Unified method for creating and updating messages""" try: - self._messages.append({"role": "user", "content": PROMPT.format(USER_PROMPT=prompt)}) - response = self.generate_message() + messages: List[MessageParam] = self.cached_docs.copy() + if history: + messages += history + + messages.append({"role": "user", "content": PROMPT.format(USER_PROMPT=prompt)}) + response = self._process_message(user_id=user_id, messages=messages, message_type=message_type) + if response.content[-1].type == "tool_use": tool_use = response.content[-1] - self._messages.append({"role": "assistant", "content": response.content}) + messages.append({"role": "assistant", "content": response.content}) result = self.handle_tool_use(tool_use.name, tool_use.input) # type: ignore - self._messages.append( + messages.append( { "role": "user", "content": [ @@ -120,25 +135,26 @@ def predict(self, prompt: str) -> str | None: ], } ) - follow_up = self.generate_message() - response_text = follow_up.content[0].text # type: ignore - self._messages.append({"role": "assistant", "content": response_text}) - return response_text + follow_up = self._process_message(user_id=user_id, messages=messages, message_type=message_type) + return follow_up.content[0].text # type: ignore elif response.content[0].type == "text": - response_text = response.content[0].text - self._messages.append({"role": "assistant", "content": response_text}) - return response_text + return response.content[0].text logger.error("Unexpected response type") return None - except IndexError as e: - logger.error("Invalid response format", extra={"error": str(e)}) - return None except Exception as e: - logger.error("Error in predict method", extra={"error": str(e)}) + logger.error(f"Error in {message_type} method", extra={"error": str(e)}) return None + @tracer.wrap() + def create(self, user_id: str, prompt: str, history: List[MessageParam] | None = None) -> str | None: + return self.process_message(user_id, prompt, history, "create") + + @tracer.wrap() + def update(self, user_id: str, prompt: str, history: List[MessageParam] | None = None) -> str | None: + return self.process_message(user_id, prompt, history, "update") + @tracer.wrap() def handle_tool_use(self, func_name: str, func_params: Dict[str, Any]) -> str: if func_name == "simulate_protocol": @@ -148,17 +164,6 @@ def handle_tool_use(self, func_name: str, func_params: Dict[str, Any]) -> str: logger.error("Unknown tool", extra={"tool": func_name}) raise ValueError(f"Unknown tool: {func_name}") - @tracer.wrap() - def reset(self) -> None: - self._messages = [ - { - "role": "user", - "content": [ - {"type": "text", "text": DOCUMENTS.format(doc_content=self.get_docs()), "cache_control": {"type": "ephemeral"}} # type: ignore - ], - } - ] - @tracer.wrap() def simulate_protocol(self, protocol: str) -> str: url = "https://Opentrons-simulator.hf.space/protocol" @@ -197,8 +202,9 @@ def main() -> None: settings = Settings() llm = AnthropicPredict(settings) - prompt = Prompt.ask("Type a prompt to send to the Anthropic API:") - completion = llm.predict(prompt) + Prompt.ask("Type a prompt to send to the Anthropic API:") + + completion = llm.create(user_id="1", prompt="hi", history=None) print(completion) diff --git a/opentrons-ai-server/api/domain/config_anthropic.py b/opentrons-ai-server/api/domain/config_anthropic.py index 9d511012592..beebc16d5ec 100644 --- a/opentrons-ai-server/api/domain/config_anthropic.py +++ b/opentrons-ai-server/api/domain/config_anthropic.py @@ -4,14 +4,11 @@ Your key responsibilities: 1. Welcome scientists warmly and understand their protocol needs -2. Generate accurate Python protocols using standard Opentrons labware +2. Generate accurate Python protocols using standard Opentrons labware (see standard-loadname-info.md in ) 3. Provide clear explanations and documentation 4. Flag potential safety or compatibility issues 5. Suggest protocol optimizations when appropriate -Call protocol simulation tool to validate the code - only when it is called explicitly by the user. -For all other queries, provide direct responses. - Important guidelines: - Always verify labware compatibility before generating protocols - Include appropriate error handling in generated code @@ -28,26 +25,25 @@ """ PROMPT = """ -Here are the inputs you will work with: - - -{USER_PROMPT} - - Follow these instructions to handle the user's prompt: -1. Analyze the user's prompt to determine if it's: +1. : a) A request to generate a protocol - b) A question about the Opentrons Python API v2 + b) A question about the Opentrons Python API v2 or about details of protocol c) A common task (e.g., value changes, OT-2 to Flex conversion, slot correction) d) An unrelated or unclear request + e) A tool calling. If a user calls simulate protocol explicity, then call. + f) A greeting. Respond kindly. -2. If the prompt is unrelated or unclear, ask the user for clarification. For example: - I apologize, but your prompt seems unclear. Could you please provide more details? + Note: when you respond you dont need mention the category or the type. +2. If the prompt is unrelated or unclear, ask the user for clarification. + I'm sorry, but your prompt seems unclear. Could you please provide more details? + You dont need to mention -3. If the prompt is a question about the API, answer it using only the information + +3. If the prompt is a question about the API or details, answer it using only the information provided in the section. Provide references and place them under the tag. Format your response like this: API answer: @@ -86,8 +82,8 @@ }} requirements = {{ - 'robotType': '[Robot type based on user prompt, OT-2 or Flex, default is OT-2]', - 'apiLevel': '[apiLevel, default is 2.19 ]' + 'robotType': '[Robot type: OT-2(default) for Opentrons OT-2, Flex for Opentrons Flex]', + 'apiLevel': '[apiLevel, default: 2.19]' }} def run(protocol: protocol_api.ProtocolContext): @@ -214,4 +210,10 @@ def run(protocol: protocol_api.ProtocolContext): as a reference to generate a basic protocol. Remember to use only the information provided in the . Do not introduce any external information or assumptions. + +Here are the inputs you will work with: + + +{USER_PROMPT} + """ diff --git a/opentrons-ai-server/api/handler/fast.py b/opentrons-ai-server/api/handler/fast.py index b93eb6580ce..a167693dc2c 100644 --- a/opentrons-ai-server/api/handler/fast.py +++ b/opentrons-ai-server/api/handler/fast.py @@ -199,10 +199,19 @@ async def create_chat_completion( return ChatResponse(reply="Default fake response. ", fake=body.fake) response: Optional[str] = None + + if body.history and body.history[0].get("content") and "Write a protocol using" in body.history[0]["content"]: # type: ignore + protocol_option = "create" + else: + protocol_option = "update" + if "openai" in settings.model.lower(): response = openai.predict(prompt=body.message, chat_completion_message_params=body.history) else: - response = claude.predict(prompt=body.message) + if protocol_option == "create": + response = claude.create(user_id=str(user.sub), prompt=body.message, history=body.history) # type: ignore + else: + response = claude.update(user_id=str(user.sub), prompt=body.message, history=body.history) # type: ignore if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -218,35 +227,36 @@ async def create_chat_completion( @tracer.wrap() @app.post( - "/api/chat/updateProtocol", + "/api/chat/createProtocol", response_model=Union[ChatResponse, ErrorResponse], - summary="Updates protocol", - description="Generate a chat response based on the provided prompt that will update an existing protocol with the required changes.", + summary="Creates protocol", + description="Generate a chat response based on the provided prompt that will create a new protocol with the required changes.", ) -async def update_protocol( - body: UpdateProtocol, user: Annotated[User, Security(auth.verify)] +async def create_protocol( + body: CreateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ Generate an updated protocol using LLM. - - **request**: The HTTP request containing the existing protocol and other relevant parameters. + - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. """ - logger.info("POST /api/chat/updateProtocol", extra={"body": body.model_dump(), "user": user}) + logger.info("POST /api/chat/createProtocol", extra={"body": body.model_dump(), "user": user}) try: - if not body.protocol_text or body.protocol_text == "": + + if not body.prompt or body.prompt == "": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=EmptyRequestError(message="Request body is empty").model_dump() ) if body.fake: - return ChatResponse(reply="Fake response", fake=bool(body.fake)) + return ChatResponse(reply="Fake response", fake=body.fake) response: Optional[str] = None if "openai" in settings.model.lower(): - response = openai.predict(prompt=body.prompt, chat_completion_message_params=None) + response = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) else: - response = claude.predict(prompt=body.prompt) + response = claude.create(user_id=str(user.sub), prompt=body.prompt, history=None) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -254,7 +264,7 @@ async def update_protocol( return ChatResponse(reply=response, fake=bool(body.fake)) except Exception as e: - logger.exception("Error processing protocol update") + logger.exception("Error processing protocol creation") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=InternalServerError(exception_object=e).model_dump() ) from e @@ -262,36 +272,35 @@ async def update_protocol( @tracer.wrap() @app.post( - "/api/chat/createProtocol", + "/api/chat/updateProtocol", response_model=Union[ChatResponse, ErrorResponse], - summary="Creates protocol", - description="Generate a chat response based on the provided prompt that will create a new protocol with the required changes.", + summary="Updates protocol", + description="Generate a chat response based on the provided prompt that will update an existing protocol with the required changes.", ) -async def create_protocol( - body: CreateProtocol, user: Annotated[User, Security(auth.verify)] +async def update_protocol( + body: UpdateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ Generate an updated protocol using LLM. - - **request**: The HTTP request containing the chat message. + - **request**: The HTTP request containing the existing protocol and other relevant parameters. - **returns**: A chat response or an error message. """ - logger.info("POST /api/chat/createProtocol", extra={"body": body.model_dump(), "user": user}) + logger.info("POST /api/chat/updateProtocol", extra={"body": body.model_dump(), "user": user}) try: - - if not body.prompt or body.prompt == "": + if not body.protocol_text or body.protocol_text == "": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=EmptyRequestError(message="Request body is empty").model_dump() ) if body.fake: - return ChatResponse(reply="Fake response", fake=body.fake) + return ChatResponse(reply="Fake response", fake=bool(body.fake)) response: Optional[str] = None if "openai" in settings.model.lower(): - response = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) + response = openai.predict(prompt=body.prompt, chat_completion_message_params=None) else: - response = claude.predict(prompt=str(body.model_dump())) + response = claude.update(user_id=str(user.sub), prompt=body.prompt, history=None) if response is None or response == "": return ChatResponse(reply="No response was generated", fake=bool(body.fake)) @@ -299,7 +308,7 @@ async def create_protocol( return ChatResponse(reply=response, fake=bool(body.fake)) except Exception as e: - logger.exception("Error processing protocol creation") + logger.exception("Error processing protocol update") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=InternalServerError(exception_object=e).model_dump() ) from e diff --git a/opentrons-ai-server/api/models/chat_request.py b/opentrons-ai-server/api/models/chat_request.py index fb8c0942c9d..6135ba0618c 100644 --- a/opentrons-ai-server/api/models/chat_request.py +++ b/opentrons-ai-server/api/models/chat_request.py @@ -24,9 +24,13 @@ class Chat(BaseModel): Field(None, description="Chat history in the form of a list of messages. Type is from OpenAI's ChatCompletionMessageParam"), ] +ChatOptions = Literal["update", "create"] +ChatOptionType = Annotated[Optional[ChatOptions], Field("create", description="which chat pathway did the user enter: create or update")] + class ChatRequest(BaseModel): message: str = Field(..., description="The latest message to be processed.") history: HistoryType fake: bool = Field(True, description="When set to true, the response will be a fake. OpenAI API is not used.") fake_key: FakeKeyType + chat_options: ChatOptionType diff --git a/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md b/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md deleted file mode 100644 index f4b54d4308a..00000000000 --- a/opentrons-ai-server/api/storage/docs/standard-api-v0.0.1.md +++ /dev/null @@ -1,157 +0,0 @@ -Standard API - -### Approved Pipette Loadnames - -Note that the labware names are hard to differentiate sometimes, -since there are cases that they differ in terms of last digits only. - -#### OT-2 Approved Loadnames - -For OT-2 robots, use the following approved loadnames: - -- p20_single_gen2 -- p300_single_gen2 -- p1000_single_gen2 -- p300_multi_gen2 -- p20_multi_gen2 - -#### Flex Approved Loadnames - -For Flex robots, use these approved loadnames: - -- flex_1channel_50 -- flex_1channel_1000 -- flex_8channel_50 -- flex_8channel_1000 -- flex_96channel_1000 - -### Agilent Labware - -- Agilent 1 Well Reservoir 290 mL: agilent_1_reservoir_290ml - -### Applied Biosystems Labware - -- Applied Biosystems MicroAmp 384 Well Plate 40 uL: appliedbiosystemsmicroamp_384_wellplate_40ul - -### Axygen Labware - -- Axygen 1 Well Reservoir 90 mL: axygen_1_reservoir_90ml - -### Bio-Rad Labware - -- Bio-Rad 384 Well Plate 50 uL: biorad_384_wellplate_50ul -- Bio-Rad 96 Well Plate 200 uL PCR: biorad_96_wellplate_200ul_pcr - -### Corning Labware - -- Corning 12 Well Plate 6.9 mL Flat: corning_12_wellplate_6.9ml_flat -- Corning 24 Well Plate 3.4 mL Flat: corning_24_wellplate_3.4ml_flat -- Corning 384 Well Plate 112 uL Flat: corning_384_wellplate_112ul_flat -- Corning 48 Well Plate 1.6 mL Flat: corning_48_wellplate_1.6ml_flat -- Corning 6 Well Plate 16.8 mL Flat: corning_6_wellplate_16.8ml_flat -- Corning 96 Well Plate 360 uL Flat: corning_96_wellplate_360ul_flat - -### GEB Labware - -- GEB 96 Tip Rack 1000 uL: geb_96_tiprack_1000ul -- GEB 96 Tip Rack 10 uL: geb_96_tiprack_10ul - -### NEST Labware - -- NEST 12 Well Reservoir 15 mL: nest_12_reservoir_15ml -- NEST 1 Well Reservoir 195 mL: nest_1_reservoir_195ml -- NEST 1 Well Reservoir 290 mL: nest_1_reservoir_290ml -- NEST 96 Well Plate 100 uL PCR Full Skirt: nest_96_wellplate_100ul_pcr_full_skirt -- NEST 96 Well Plate 200 uL Flat: nest_96_wellplate_200ul_flat -- NEST 96 Deep Well Plate 2mL: nest_96_wellplate_2ml_deep - -### Opentrons Labware - -- Opentrons 10 Tube Rack with Falcon 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical -- Opentrons 10 Tube Rack with NEST 4x50 mL, 6x15 mL Conical: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical -- Opentrons 15 Tube Rack with Falcon 15 mL Conical: opentrons_15_tuberack_falcon_15ml_conical -- Opentrons 15 Tube Rack with NEST 15 mL Conical: opentrons_15_tuberack_nest_15ml_conical -- Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap: opentrons_24_aluminumblock_generic_2ml_screwcap -- Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap: opentrons_24_aluminumblock_nest_0.5ml_screwcap -- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Screwcap: opentrons_24_aluminumblock_nest_1.5ml_screwcap -- Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap: opentrons_24_aluminumblock_nest_1.5ml_snapcap -- Opentrons 24 Well Aluminum Block with NEST 2 mL Screwcap: opentrons_24_aluminumblock_nest_2ml_screwcap -- Opentrons 24 Well Aluminum Block with NEST 2 mL Snapcap: opentrons_24_aluminumblock_nest_2ml_snapcap -- Opentrons 24 Tube Rack with Eppendorf 1.5 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap -- Opentrons 24 Tube Rack with Eppendorf 2 mL Safe-Lock Snapcap: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap -- Opentrons 24 Tube Rack with Generic 2 mL Screwcap: opentrons_24_tuberack_generic_2ml_screwcap -- Opentrons 24 Tube Rack with NEST 0.5 mL Screwcap: opentrons_24_tuberack_nest_0.5ml_screwcap # not opentrons_24_tuberack_nest_0_5ml_screwcap -- Opentrons 24 Tube Rack with NEST 1.5 mL Screwcap: opentrons_24_tuberack_nest_1.5ml_screwcap # not opentrons_24_tuberack_nest_1_5ml_screwcap -- Opentrons 24 Tube Rack with NEST 1.5 mL Snapcap: opentrons_24_tuberack_nest_1.5ml_snapcap # note the use of dot. (`.`); opentrons_24_tuberack_nest_1_5ml_snapcap is incorrect -- Opentrons 24 Tube Rack with NEST 2 mL Screwcap: opentrons_24_tuberack_nest_2ml_screwcap -- Opentrons 24 Tube Rack with NEST 2 mL Snapcap: opentrons_24_tuberack_nest_2ml_snapcap -- Opentrons 6 Tube Rack with Falcon 50 mL Conical: opentrons_6_tuberack_falcon_50ml_conical -- Opentrons 6 Tube Rack with NEST 50 mL Conical: opentrons_6_tuberack_nest_50ml_conical -- Opentrons 96 Well Aluminum Block with Bio-Rad Well Plate 200 uL: opentrons_96_aluminumblock_biorad_wellplate_200ul -- Opentrons 96 Well Aluminum Block with Generic PCR Strip 200 uL: opentrons_96_aluminumblock_generic_pcr_strip_200ul -- Opentrons 96 Well Aluminum Block with NEST Well Plate 100 uL: opentrons_96_aluminumblock_nest_wellplate_100ul -- Opentrons 96 Deep Well Heater-Shaker Adapter: opentrons_96_deep_well_adapter -- Opentrons 96 Deep Well Heater-Shaker Adapter with NEST Deep Well Plate 2 mL: opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep -- Opentrons OT-2 96 Filter Tip Rack 1000 uL: opentrons_96_filtertiprack_1000ul -- Opentrons OT-2 96 Filter Tip Rack 10 uL: opentrons_96_filtertiprack_10ul -- Opentrons OT-2 96 Filter Tip Rack 200 uL: opentrons_96_filtertiprack_200ul -- Opentrons OT-2 96 Filter Tip Rack 20 uL: opentrons_96_filtertiprack_20ul -- Opentrons 96 Flat Bottom Heater-Shaker Adapter: opentrons_96_flat_bottom_adapter -- Opentrons 96 Flat Bottom Heater-Shaker Adapter with NEST 96 Well Plate 200 uL Flat: opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat -- Opentrons 96 PCR Heater-Shaker Adapter: opentrons_96_pcr_adapter -- Opentrons 96 PCR Heater-Shaker Adapter with NEST Well Plate 100 ul: opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt -- Opentrons OT-2 96 Tip Rack 1000 uL: opentrons_96_tiprack_1000ul -- Opentrons OT-2 96 Tip Rack 10 uL: opentrons_96_tiprack_10ul -- Opentrons OT-2 96 Tip Rack 20 uL: opentrons_96_tiprack_20ul -- Opentrons OT-2 96 Tip Rack 300 uL: opentrons_96_tiprack_300ul -- Opentrons 96 Well Aluminum Block: opentrons_96_well_aluminum_block -- Opentrons 96 Well Aluminum Block adapter: opentrons_96_well_aluminum_block -- Opentrons Tough 96 Well Plate 200 uL PCR Full Skirt: opentrons_96_wellplate_200ul_pcr_full_skirt -- Opentrons Aluminum Flat Bottom Plate: opentrons_aluminum_flat_bottom_plate -- Opentrons Flex 96 Filter Tip Rack 1000 uL: opentrons_flex_96_filtertiprack_1000ul # note that 1000ul not 200ul -- Opentrons Flex 96 Filter Tip Rack 200 uL: opentrons_flex_96_filtertiprack_200ul # note that 200ul not 1000ul -- Opentrons Flex 96 Filter Tip Rack 50 uL: opentrons_flex_96_filtertiprack_50ul -- Opentrons Flex 96 Tip Rack 1000 uL: opentrons_flex_96_tiprack_1000ul -- Opentrons Flex 96 Tip Rack 200 uL: opentrons_flex_96_tiprack_200ul -- Opentrons Flex 96 Tip Rack 50 uL: opentrons_flex_96_tiprack_50ul -- Opentrons Flex 96 Tip Rack Adapter: opentrons_flex_96_tiprack_adapter -- Opentrons Universal Flat Heater-Shaker Adapter: opentrons_universal_flat_adapter -- Opentrons Universal Flat Heater-Shaker Adapter with Corning 384 Well Plate 112 ul Flat: opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat - -### Other Labware Brands - -- Thermo Scientific Nunc 96 Well Plate 1300 uL: thermoscientificnunc_96_wellplate_1300ul -- Thermo Scientific Nunc 96 Well Plate 2000 uL: thermoscientificnunc_96_wellplate_2000ul -- USA Scientific 12 Well Reservoir 22 mL: usascientific_12_reservoir_22ml -- USA Scientific 96 Deep Well Plate 2.4 mL: usascientific_96_wellplate_2.4ml_deep - -### Additional Opentrons Tube Racks - -- 4-in-1 Tube Rack Set 15: opentrons_15_tuberack_nest_15ml_conical -- 4-in-1 Tube Rack Set 50: opentrons_6_tuberack_nest_50ml_conical - -### Flex Pipettes - -- Flex 1-Channel 50 uL Pipette (single channel): flex_1channel_50 -- Flex 1-Channel 1000 uL Pipette (single channel): flex_1channel_1000 -- Flex 8-Channel 50 uL Pipette (multi-channel): flex_8channel_50 -- Flex 8-Channel 1000 uL Pipette (multi-channel): flex_8channel_1000 -- Flex 96-Channel 1000 uL Pipette (multi-channel): flex_96channel_1000 - -### Modules - -- temperature module: temperature module gen2 -- thermocycler module: thermocycler module -- thermocycler module gen2: thermocyclerModuleV2 - -### Single channel pipettes: - -- Flex 1-Channel 50 uL Pipette -- Flex 1-Channel 1000 uL Pipette -- flex_1channel_1000 - -### Multi channel pipettes: - -- Flex 8-Channel 50 uL Pipette -- Flex 8-Channel 1000 uL Pipette -- Flex 96-Channel 1000 uL Pipette diff --git a/opentrons-ai-server/api/storage/docs/standard-loadname-info.md b/opentrons-ai-server/api/storage/docs/standard-loadname-info.md new file mode 100644 index 00000000000..5ca402ec2f3 --- /dev/null +++ b/opentrons-ai-server/api/storage/docs/standard-loadname-info.md @@ -0,0 +1,599 @@ + +Total number of labware: 73 + + + +- Loadname: agilent_1_reservoir_290ml +- Dimensions: 1 row × 1 column +- Well count: 1 +- Max volume: 290 mL +- Well shape: V-bottom + + + +- Loadname: appliedbiosystemsmicroamp_384_wellplate_40ul +- Dimensions: 16 rows × 24 columns +- Well count: 384 +- Max volume: 40 µL +- Well shape: V-bottom +- Note: Requires Opentrons software v5.0 or later + + + +- Loadname: axygen_1_reservoir_90ml +- Dimensions: 1 row × 1 column +- Well count: 1 +- Max volume: 90 mL +- Well shape: Flat-bottom + + + +- Loadname: biorad_384_wellplate_50ul +- Dimensions: 16 rows × 24 columns +- Well count: 384 +- Max volume: 50 µL +- Well shape: V-bottom +- Note: Requires Opentrons software v5.0 or later + + + +- Loadname: biorad_96_wellplate_200ul_pcr +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 200 µL +- Well shape: V-bottom + + + +- Loadname: corning_12_wellplate_6.9ml_flat +- Dimensions: 3 rows × 4 columns +- Well count: 12 +- Max volume: 6.9 mL +- Well shape: Flat-bottom + + + +- Loadname: corning_24_wellplate_3.4ml_flat +- Dimensions: 4 rows × 6 columns +- Well count: 24 +- Max volume: 3.4 mL +- Well shape: Flat-bottom + + + +- Loadname: corning_384_wellplate_112ul_flat +- Dimensions: 16 rows × 24 columns +- Well count: 384 +- Max volume: 112 µL +- Well shape: Flat-bottom + + + +- Loadname: corning_48_wellplate_1.6ml_flat +- Dimensions: 6 rows × 8 columns +- Well count: 48 +- Max volume: 1.6 mL +- Well shape: Flat-bottom + + + +- Loadname: corning_6_wellplate_16.8ml_flat +- Dimensions: 2 rows × 3 columns +- Well count: 6 +- Max volume: 16.8 mL +- Well shape: Flat-bottom + + + +- Loadname: corning_96_wellplate_360ul_flat +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 360 µL +- Well shape: Flat-bottom + + + +- Loadname: geb_96_tiprack_1000ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 1000 µL + + + +- Loadname: geb_96_tiprack_10ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 10 µL + + + +- Loadname: nest_12_reservoir_15ml +- Dimensions: 1 row × 12 columns +- Well count: 12 +- Max volume: 15 mL +- Well shape: V-bottom + + + +- Loadname: nest_1_reservoir_195ml +- Dimensions: 1 row × 1 column +- Well count: 1 +- Max volume: 195 mL +- Well shape: V-bottom + + + +- Loadname: nest_1_reservoir_290ml +- Dimensions: 1 row × 1 column +- Well count: 1 +- Max volume: 290 mL +- Well shape: V-bottom + + + +- Loadname: nest_96_wellplate_100ul_pcr_full_skirt +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 100 µL +- Well shape: V-bottom + + + +- Loadname: nest_96_wellplate_200ul_flat +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 200 µL +- Well shape: Flat-bottom + + + +- Loadname: nest_96_wellplate_2ml_deep +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 2000 µL +- Well shape: V-bottom + + + +- Loadname: opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical +- Tube count: 10 +- Configuration: + - 6 × 15 mL tubes (V-bottom) + - 4 × 50 mL tubes (V-bottom) + + + +- Loadname: opentrons_10_tuberack_nest_4x50ml_6x15ml_conical +- Tube count: 10 +- Configuration: + - 6 × 15 mL tubes (V-bottom) + - 4 × 50 mL tubes (V-bottom) + + + +- Loadname: opentrons_15_tuberack_falcon_15ml_conical +- Dimensions: 3 rows × 5 columns +- Tube count: 15 +- Max volume: 15 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_15_tuberack_nest_15ml_conical +- Dimensions: 3 rows × 5 columns +- Tube count: 15 +- Max volume: 15 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_generic_2ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_nest_0.5ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 0.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_nest_1.5ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 1.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_nest_1.5ml_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 1.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_nest_2ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_aluminumblock_nest_2ml_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: U-bottom + + + +- Loadname: opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 1.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: U-bottom + + + +- Loadname: opentrons_24_tuberack_generic_2ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_nest_0.5ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 0.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_nest_1.5ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 1.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_nest_1.5ml_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 1.5 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_nest_2ml_screwcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_24_tuberack_nest_2ml_snapcap +- Dimensions: 4 rows × 6 columns +- Tube count: 24 +- Max volume: 2 mL per tube +- Tube shape: U-bottom + + + +- Loadname: opentrons_6_tuberack_falcon_50ml_conical +- Dimensions: 2 rows × 3 columns +- Tube count: 6 +- Max volume: 50 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_6_tuberack_nest_50ml_conical +- Dimensions: 2 rows × 3 columns +- Tube count: 6 +- Max volume: 50 mL per tube +- Tube shape: V-bottom + + + +- Loadname: opentrons_96_deep_well_temp_mod_adapter +- Dimensions: 8 rows × 12 columns +- Well count: 0 +- Max volume: Various +- Note: Adapter only + + + +- Loadname: opentrons_96_aluminumblock_biorad_wellplate_200ul +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 200 µL +- Well shape: V-bottom + + + +- Loadname: opentrons_96_aluminumblock_generic_pcr_strip_200ul +- Dimensions: 8 rows × 12 columns +- Tube count: 96 +- Max volume: 200 µL +- Tube shape: V-bottom + + + +- Loadname: opentrons_96_aluminumblock_nest_wellplate_100ul +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 100 µL +- Well shape: V-bottom + + + +- Loadname: opentrons_96_deep_well_adapter +- Dimensions: 8 rows × 12 columns +- Well count: 0 +- Max volume: Various +- Note: Adapter only + + + +- Loadname: opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 2000 µL +- Well shape: V-bottom + + + +- Loadname: opentrons_96_filtertiprack_1000ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 1000 µL + + + +- Loadname: opentrons_96_filtertiprack_10ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 10 µL + + + +- Loadname: opentrons_96_filtertiprack_200ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 200 µL + + + +- Loadname: opentrons_96_filtertiprack_20ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 20 µL + + + +- Loadname: opentrons_96_flat_bottom_adapter +- Dimensions: 8 rows × 12 columns +- Well count: 0 +- Max volume: Various +- Note: Adapter only + + + +- Loadname: opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 200 µL +- Well shape: Flat-bottom + + + +- Loadname: opentrons_96_pcr_adapter +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: Various +- Well shape: V-bottom + + + +- Loadname: opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 100 µL +- Well shape: V-bottom + + + +- Loadname: opentrons_96_tiprack_1000ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 1000 µL + + + +- Loadname: opentrons_96_tiprack_10ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 10 µL + + + +- Loadname: opentrons_96_tiprack_20ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 20 µL + + + +- Loadname: opentrons_96_tiprack_300ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 300 µL + + + +- Loadname: opentrons_96_well_aluminum_block +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: Various +- Well shape: V-bottom + + + +- Loadname: opentrons_96_wellplate_200ul_pcr_full_skirt +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 200 µL +- Well shape: V-bottom + + +- Loadname: opentrons_aluminum_flat_bottom_plate +- Dimensions: N/A (base plate) +- Well count: 0 +- Max volume: Various +- Note: Base plate only + + + +- Loadname: opentrons_flex_96_filtertiprack_1000ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 1000 µL + + + +- Loadname: opentrons_flex_96_filtertiprack_200ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 200 µL + + + +- Loadname: opentrons_flex_96_filtertiprack_50ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 50 µL + + + +- Loadname: opentrons_flex_96_tiprack_1000ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 1000 µL + + + +- Loadname: opentrons_flex_96_tiprack_200ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 200 µL + + + +- Loadname: opentrons_flex_96_tiprack_50ul +- Dimensions: 8 rows × 12 columns +- Tip count: 96 +- Max volume: 50 µL + + + +- Loadname: opentrons_flex_96_tiprack_adapter +- Dimensions: 8 rows × 12 columns +- Well count: 0 +- Max volume: Various +- Note: Adapter only + + +- Loadname: opentrons_universal_flat_adapter +- Dimensions: N/A (universal adapter) +- Well count: 0 +- Max volume: Various +- Note: Adapter only + + + +- Loadname: opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat +- Dimensions: 16 rows × 24 columns +- Well count: 384 +- Max volume: 112 µL +- Well shape: Flat-bottom + + + +- Loadname: thermoscientificnunc_96_wellplate_1300ul +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 1300 µL +- Well shape: U-bottom +- Note: Requires Opentrons software v5.0 or later + + + +- Loadname: thermoscientificnunc_96_wellplate_2000ul +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 2000 µL +- Well shape: U-bottom +- Note: Requires Opentrons software v5.0 or later + + + +- Loadname: usascientific_12_reservoir_22ml +- Dimensions: 1 row × 12 columns +- Well count: 12 +- Max volume: 22 mL +- Well shape: V-bottom + + + +- Loadname: usascientific_96_wellplate_2.4ml_deep +- Dimensions: 8 rows × 12 columns +- Well count: 96 +- Max volume: 2.4 mL +- Well shape: U-bottom + + + + - p20_single_gen2 + - volume: 1-20 µL + - p20_multi_gen2 + - volume: 1-20 µL + - p300_single_gen2 + - volume: 20-300 µL + - p300_multi_gen2 + - volume: 20-200 µL + - p1000_single_gen2 + - volume: 100-1000 µL + + + - flex_1channel_50 + - volume: 1–50 µL + - flex_1channel_1000 + - volume: 5–1000 µL + - flex_8channel_50 + - volume: 1–50 µL + - flex_8channel_1000 + - volume: 5–1000 µL + - flex_96channel_1000 + - volume: 5–1000 µL + + diff --git a/opentrons-ai-server/tests/helpers/client.py b/opentrons-ai-server/tests/helpers/client.py index bf5a7febb3c..3b3dcfa7511 100644 --- a/opentrons-ai-server/tests/helpers/client.py +++ b/opentrons-ai-server/tests/helpers/client.py @@ -65,7 +65,7 @@ def get_health(self) -> Response: @timeit def get_chat_completion(self, message: str, fake: bool = True, fake_key: Optional[FakeKeys] = None, bad_auth: bool = False) -> Response: """Call the /chat/completion endpoint and return the response.""" - request = ChatRequest(message=message, fake=fake, fake_key=fake_key, history=None) + request = ChatRequest(message=message, fake=fake, fake_key=fake_key, history=None, chat_options=None) headers = self.standard_headers if not bad_auth else self.invalid_auth_headers return self.httpx.post("/chat/completion", headers=headers, json=request.model_dump()) diff --git a/protocol-designer/cypress/support/commands.ts b/protocol-designer/cypress/support/commands.ts index b97c11f2bd2..3f9ffd8ddd8 100644 --- a/protocol-designer/cypress/support/commands.ts +++ b/protocol-designer/cypress/support/commands.ts @@ -45,9 +45,9 @@ export const content = { charSet: 'UTF-8', header: 'Protocol Designer', welcome: 'Welcome to Protocol Designer!', - appSettings: 'App settings', + appSettings: 'App Info', privacy: 'Privacy', - shareSessions: 'Share sessions with Opentrons', + shareSessions: 'Share analytics with Opentrons', } export const locators = { diff --git a/protocol-designer/src/ProtocolEditor.tsx b/protocol-designer/src/ProtocolEditor.tsx index df486e3eee4..7ca14592e86 100644 --- a/protocol-designer/src/ProtocolEditor.tsx +++ b/protocol-designer/src/ProtocolEditor.tsx @@ -4,11 +4,8 @@ import { HTML5Backend } from 'react-dnd-html5-backend' import { DIRECTION_COLUMN, Flex, OVERFLOW_AUTO } from '@opentrons/components' import { PortalRoot as TopPortalRoot } from './components/portals/TopPortal' import { ProtocolRoutes } from './ProtocolRoutes' -import { useScreenSizeCheck } from './resources/useScreenSizeCheck' -import { DisabledScreen } from './organisms/DisabledScreen' function ProtocolEditorComponent(): JSX.Element { - const isValidSize = useScreenSizeCheck() return (
- {!isValidSize && } diff --git a/protocol-designer/src/assets/images/onboarding_animation_1.webm b/protocol-designer/src/assets/images/onboarding_animation_1.webm new file mode 100644 index 00000000000..6eed789cb61 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_1.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_2.webm b/protocol-designer/src/assets/images/onboarding_animation_2.webm new file mode 100644 index 00000000000..7dbc51c26ad Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_2.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_3.webm b/protocol-designer/src/assets/images/onboarding_animation_3.webm new file mode 100644 index 00000000000..19d29e2b939 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_3.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_4.webm b/protocol-designer/src/assets/images/onboarding_animation_4.webm new file mode 100644 index 00000000000..d59f86faf45 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_4.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_5.webm b/protocol-designer/src/assets/images/onboarding_animation_5.webm new file mode 100644 index 00000000000..4fc1580c4f8 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_5.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_6.webm b/protocol-designer/src/assets/images/onboarding_animation_6.webm new file mode 100644 index 00000000000..ea00e3fcdb1 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_6.webm differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_ot2_2.gif b/protocol-designer/src/assets/images/onboarding_animation_ot2_2.gif new file mode 100644 index 00000000000..2ce6504b28c Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_ot2_2.gif differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_ot2_3.gif b/protocol-designer/src/assets/images/onboarding_animation_ot2_3.gif new file mode 100644 index 00000000000..e1b8cfb9291 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_ot2_3.gif differ diff --git a/protocol-designer/src/assets/images/onboarding_animation_ot2_4.gif b/protocol-designer/src/assets/images/onboarding_animation_ot2_4.gif new file mode 100644 index 00000000000..ef437881ae9 Binary files /dev/null and b/protocol-designer/src/assets/images/onboarding_animation_ot2_4.gif differ diff --git a/protocol-designer/src/assets/images/placeholder_image_delete.png b/protocol-designer/src/assets/images/placeholder_image_delete.png deleted file mode 100644 index f429a3862dc..00000000000 Binary files a/protocol-designer/src/assets/images/placeholder_image_delete.png and /dev/null differ diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index 269c252dd39..f2e132728a5 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -11,7 +11,7 @@ "edit": "Edit", "fixtures_added": "Fixtures added", "fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.", - "incompatible_tip_body": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.", + "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.", "incompatible_tips": "Incompatible tips", "labware_name": "Labware name", "left_right": "Left + Right", diff --git a/protocol-designer/src/assets/localization/en/feature_flags.json b/protocol-designer/src/assets/localization/en/feature_flags.json index 74c524de0e0..c39e10a8785 100644 --- a/protocol-designer/src/assets/localization/en/feature_flags.json +++ b/protocol-designer/src/assets/localization/en/feature_flags.json @@ -25,7 +25,7 @@ "description": "You can choose which tip to pick up and where to drop tip." }, "OT_PD_ENABLE_HOT_KEYS_DISPLAY": { - "title": "Timeline editing tips", - "description": "Show tips for working with steps next to the protocol timeline" + "title": "Timeline editing guidance", + "description": "Show information about working with steps next to the protocol timeline" } } diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 6105b29b24d..2f498904c61 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -27,7 +27,7 @@ "duplicate": "Duplicate step", "edit_step": "Edit step", "engage_height": "Engage height", - "final_deck_state": "Final deck state", + "ending_deck": "Ending deck", "flow_type_title": "{{type}} flow rate", "from": "from", "heater_shaker": { @@ -44,7 +44,7 @@ "open": "open" } }, - "heater_shaker_settings": "Heater-shaker settings", + "heater_shaker_settings": "Heater-Shaker Settings", "in": "in", "into": "into", "magnetic_module": { @@ -104,7 +104,7 @@ "shake": "Shake", "single": "Single path", "speed": "Speed", - "starting_deck_state": "Starting deck state", + "starting_deck": "Starting deck", "step_substeps": "{{stepType}} details", "temperature": "Temperature", "temperature_module": { @@ -120,8 +120,8 @@ "substep_settings": "Set block temperature tofor", "thermocycler_profile": { "end_hold": { - "block": "End at thermocycler block", - "lid_position": "Thermocycler lid" + "block": "End with block at", + "lid_position": "and lid" }, "lid_temperature": "and lid temperature at", "volume": "Run thermocycler profile with" diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index 3a6545622a5..58579da36ff 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -3,7 +3,7 @@ "agree": "Agree", "amount": "Amount:", "analytics_tracking": "I consent to analytics tracking:", - "app_settings": "App settings", + "app_info": "App Info", "ask_for_labware_overwrite": "Duplicate labware name", "back": "Back", "cancel": "Cancel", @@ -17,7 +17,7 @@ "create_a_protocol": "Create a protocol", "create_new": "Create new", "destination_well": "Destination Well", - "developer_ff": "Developer feature flags", + "developer_ff": "Developer Feature Flags", "done": "Done", "edit_existing": "Edit existing protocol", "edit_instruments": "Edit Instruments", @@ -84,7 +84,7 @@ }, "message_exact_labware_match": "This labware is identical to one you have already uploaded.", "message_invalid_json_file": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator", - "message_not_json": "Protocol Designer only accepts JSON files.", + "message_not_json": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.", "message_only_tiprack": "This labware definition is not a tip rack.", "message_uses_standard_namespace": "This labware definition uses the Opentrons standard labware namespace. Change the namespace if it is custom, or use the standard labware in your protocol.", "mismatched": "The new labware has a different arrangement of wells than the labware it is replacing. Clicking Overwrite will deselect all wells in any existing steps that use this labware. You will have to edit each of those steps and select new wells.", @@ -96,7 +96,7 @@ "no-code-required": "The easiest way to automate liquid handling on your Opentrons robot. No code required.", "no": "No", "none": "None", - "not_json": "Incompatible file type", + "not_json": "Invalid file type", "one_channel": "1-Channel", "only_tiprack": "Incompatible file type", "opentrons_flex": "Opentrons Flex", @@ -116,8 +116,8 @@ "release_notes": "Release notes", "reload_app": "Reload app", "remove": "remove", - "reset_hints_and_tips": "Reset all hints and tips notifications", - "reset_hints": "Reset hints", + "show_hints_and_tips": "Show all hints and tips notifications again", + "reset": "Reset", "reset_to_default": "Reset to default", "resize_your_browser": "Resize your browser to at least 768px wide and 650px tall to continue editing your protocol", "review_our_privacy_policy": "You can adjust this setting at any time by clicking on the settings icon. Find detailed information in our privacy policy.", @@ -127,7 +127,7 @@ "settings": "Settings", "shared_display_name": "Shared display name: ", "shared_load_name": "Shared load name: ", - "shared_sessions": "Share sessions with Opentrons", + "shared_analytics": "Share analytics with Opentrons", "shares_name": "This labware has the same load name or display name as {{customOrStandard}}, which is already in this protocol.", "slot_detail": "Slot Detail", "software_manual": "Software manual", @@ -147,14 +147,14 @@ "tip_position": "Edit {{prefix}} tip position", "trashBin": "Trash Bin", "updated_protocol_designer": "We've updated Protocol Designer!", - "user_settings": "User settings", + "user_settings": "User Settings", "uses_standard_namespace": "Opentrons verified labware", "version": "Version {{version}}", "view_release_notes": "View release notes", "warning": "WARNING:", "wasteChute": "Waste chute", "wasteChuteAndStagingArea": "Waste chute and staging area slot", - "we_are_improving": "In order to improve our products, Opentrons would like to collect data related to your use of Protocol Designer. With your consent, Opentrons will collect and store analytics and session data, including through the use of cookies and similar technologies, solely for the purpose enhancing our products. Find detailed information in our privacy policy. By using Protocol Designer, you consent to the Opentrons EULA.", + "we_are_improving": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", "welcome": "Welcome to Protocol Designer!", "yes": "Yes", "your_screen_is_too_small": "Your browser size is too small" diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index fcf88c2866e..cbf5e74587c 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -1,4 +1,7 @@ { + "__end__": "Ending deck", + "__initial_setup__": "Starting deck", + "__presaved_step__": "Unsaved step", "adapter_compatible_lab": "Adapter compatible labware", "adapter": "Adapters", "add_fixture": "Add a fixture", @@ -13,7 +16,7 @@ "clear_labware": "Clear labware", "clear_slot": "Clear slot", "clear": "Clear", - "command_click_to_multi_select": "Command + Click for multi-select", + "command_click_to_multi_select": "Command + click to select multiple", "convert_gen1_to_gen2": "To convert engage heights from GEN1 to GEN2, divide your engage height by 2.", "convert_gen2_to_gen1": "To convert engage heights from GEN2 to GEN1, multiply your engage height by 2.", "custom": "Custom labware definitions", @@ -48,7 +51,7 @@ "read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules", "rename_lab": "Rename labware", "reservoir": "Reservoirs", - "shift_click_to_select_all": "Shift + Click to select all", + "shift_click_to_select_range": "Shift + click to select range", "starting_deck_state": "Starting deck state", "tc_slots_occupied_flex": "The Thermocycler needs slots A1 and B1. Slot A1 is occupied", "tc_slots_occupied_ot2": "The Thermocycler needs slots 7, 8, 10, and 11. One or more of those slots is occupied", diff --git a/protocol-designer/src/atoms/constants.ts b/protocol-designer/src/atoms/constants.ts index e04701a7639..620a3d10dbc 100644 --- a/protocol-designer/src/atoms/constants.ts +++ b/protocol-designer/src/atoms/constants.ts @@ -1,11 +1,8 @@ -import styled, { css } from 'styled-components' +import { css } from 'styled-components' import { - BORDERS, COLORS, DIRECTION_COLUMN, OVERFLOW_HIDDEN, - SPACING, - TYPOGRAPHY, } from '@opentrons/components' import type { FlattenSimpleInterpolation } from 'styled-components' @@ -35,40 +32,3 @@ export const COLUMN_STYLE = css` min-width: calc((${MIN_OVERVIEW_WIDTH} - ${COLUMN_GRID_GAP}) * 0.5); flex: 1; ` - -export const DescriptionField = styled.textarea` - min-height: 5rem; - width: 100%; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; - border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeP}; - resize: none; - - &:active:enabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:hover { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey60}; - } - - &:focus-visible { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey55}; - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 2px; - } - - &:focus-within { - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - } - - &:disabled { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } - input[type='number']::-webkit-inner-spin-button, - input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } -` diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index dedfae883d8..38cccb252fb 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -73,7 +73,6 @@ export const createContainer: ( args.slot || getNextAvailableDeckSlot(initialDeckSetup, robotType, labwareDef) const isTiprack = getIsTiprack(labwareDef) - if (slot) { const id = `${uuid()}:${args.labwareDefURI}` const adapterId = diff --git a/protocol-designer/src/labware-ingred/utils.ts b/protocol-designer/src/labware-ingred/utils.ts index d4c6dc5e1bf..377dff50eb5 100644 --- a/protocol-designer/src/labware-ingred/utils.ts +++ b/protocol-designer/src/labware-ingred/utils.ts @@ -1,5 +1,6 @@ import { FIXED_TRASH_ID, + FLEX_MODULE_ADDRESSABLE_AREAS, getAreSlotsAdjacent, getDeckDefFromRobotType, getIsLabwareAboveHeight, @@ -7,6 +8,7 @@ import { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, MOVABLE_TRASH_ADDRESSABLE_AREAS, OT2_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, WASTE_CHUTE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from '@opentrons/step-generation' @@ -30,6 +32,16 @@ export function getNextAvailableDeckSlot( module => module.type === HEATERSHAKER_MODULE_TYPE )?.slot + const hasTC = Object.values(initialDeckSetup.modules).find( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + let moduleSlots = Object.values(initialDeckSetup.modules) + .filter(module => module.slot) + .map(mod => mod.slot) + if (hasTC) { + moduleSlots = [...moduleSlots, '8', '10', '11'] + } + return deckDef.locations.addressableAreas.find(slot => { const cutoutIds = Object.values(initialDeckSetup.additionalEquipmentOnDeck) .filter(ae => ae.name === 'stagingArea') @@ -47,12 +59,17 @@ export function getNextAvailableDeckSlot( MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slot.id) || WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slot.id) || slot.id === FIXED_TRASH_ID + ) { + isSlotEmpty = false + } else if ( + moduleSlots.includes(slot.id) || + FLEX_MODULE_ADDRESSABLE_AREAS.includes(slot.id) ) { isSlotEmpty = false // return slot as full if slot is adjacent to heater-shaker for ot-2 and taller than 53mm } else if ( heaterShakerSlot != null && - deckDef.robot.model === OT2_ROBOT_TYPE && + robotType === OT2_ROBOT_TYPE && isSlotEmpty && labwareDefinition != null ) { diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx index fb1d775b702..35bf74003a8 100644 --- a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx +++ b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx @@ -5,8 +5,10 @@ import { SketchPicker } from 'react-color' import { yupResolver } from '@hookform/resolvers/yup' import * as Yup from 'yup' import { Controller, useForm } from 'react-hook-form' +import styled from 'styled-components' import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data' import { + BORDERS, Btn, COLORS, DIRECTION_COLUMN, @@ -28,7 +30,7 @@ import * as labwareIngredActions from '../../labware-ingred/actions' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { swatchColors } from '../../components/swatchColors' import { HandleEnter } from '../../atoms/HandleEnter' -import { DescriptionField, LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' import type { ColorResult, RGBColor } from 'react-color' import type { ThunkDispatch } from 'redux-thunk' @@ -310,3 +312,13 @@ export function DefineLiquidsModal( ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/organisms/DisabledScreen/index.tsx b/protocol-designer/src/organisms/DisabledScreen/index.tsx index 779ba446f40..2815609b469 100644 --- a/protocol-designer/src/organisms/DisabledScreen/index.tsx +++ b/protocol-designer/src/organisms/DisabledScreen/index.tsx @@ -15,6 +15,9 @@ import { } from '@opentrons/components' import { getTopPortalEl } from '../../components/portals/TopPortal' +// Note: We decided not to use this component for the release. +// We will find out a better way to handle responsiveness with user's screen size issue. +// This component may be used in the future. If not, we will remove it. export function DisabledScreen(): JSX.Element { const { t } = useTranslation('shared') diff --git a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx index c986c1ce9ba..5aac9eeb3f1 100644 --- a/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx +++ b/protocol-designer/src/organisms/EditInstrumentsModal/index.tsx @@ -89,7 +89,7 @@ export function EditInstrumentsModal( ): JSX.Element { const { onClose } = props const dispatch = useDispatch>() - const { t } = useTranslation([ + const { i18n, t } = useTranslation([ 'create_new_protocol', 'protocol_overview', 'shared', @@ -347,7 +347,7 @@ export function EditInstrumentsModal( desktopStyle="bodyDefaultRegular" color={COLORS.grey60} > - {t('gripper')} + {i18n.format(t('gripper'), 'capitalize')} - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + {' '} )} @@ -594,4 +596,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx index 96732965dea..2a1a8cf7e4e 100644 --- a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx +++ b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx @@ -26,7 +26,7 @@ describe('IncompatibleTipsModal', () => { render(props) screen.getByText('Incompatible tips') screen.getByText( - 'Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.' + 'Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.' ) fireEvent.click(screen.getByText('Show more tip types')) expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled() diff --git a/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx b/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx index 0a7aba37a2f..4d3277453b6 100644 --- a/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx +++ b/protocol-designer/src/organisms/LabwareUploadModal/_tests__/LabwareUploadModal.test.tsx @@ -24,10 +24,12 @@ describe('LabwareUploadModal', () => { it('renders modal for not json', () => { render() - screen.getByText('Protocol Designer only accepts JSON files.') - screen.getByText('Incompatible file type') + screen.getByText( + 'Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.' + ) + screen.getByText('Invalid file type') fireEvent.click( - screen.getByTestId('ModalHeader_icon_close_Incompatible file type') + screen.getByTestId('ModalHeader_icon_close_Invalid file type') ) expect(vi.mocked(dismissLabwareUploadMessage)).toHaveBeenCalled() }) diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx index 83ad6eadc5c..07196f3d78b 100644 --- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx +++ b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx @@ -96,7 +96,7 @@ export function ProtocolNavBar({ } const NavContainer = styled(Flex)<{ showShadow: boolean }>` - z-index: 11; + z-index: ${props => (props.showShadow === true ? 11 : 0)}; padding: ${SPACING.spacing12}; width: 100%; justify-content: ${JUSTIFY_SPACE_BETWEEN}; diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index 7c1a1841a22..37a080ac885 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -63,7 +63,11 @@ export const SlotInformation: FC = ({ diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx index afb8e83e9c7..d622b0ab626 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/AddMetadata.tsx @@ -1,16 +1,18 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' +import styled from 'styled-components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { + BORDERS, COLORS, DIRECTION_COLUMN, Flex, InputField, SPACING, StyledText, + TYPOGRAPHY, } from '@opentrons/components' -import { DescriptionField } from '../../atoms' import { HandleEnter } from '../../atoms/HandleEnter' import { analyticsEvent } from '../../analytics/actions' import { ONBOARDING_FLOW_DURATION_EVENT } from '../../analytics/constants' @@ -43,6 +45,7 @@ export function AddMetadata(props: AddMetadataProps): JSX.Element | null { return ( ) } + +export const DescriptionField = styled.textarea` + min-height: 5rem; + width: 100%; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeP}; + resize: none; +` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 68223dbc183..400da9a6235 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' import without from 'lodash/without' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -63,6 +64,7 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { return ( { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 3f48e08e6bd..57f2ec6b4b5 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -14,6 +14,7 @@ import { Box, Btn, Checkbox, + COLORS, CURSOR_POINTER, DIRECTION_COLUMN, DIRECTION_ROW, @@ -169,6 +170,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { ) : null} - - {allowAllTipracks - ? t('show_default_tips') - : t('show_all_tips')} - + + + {allowAllTipracks + ? t('show_default_tips') + : t('show_all_tips')} + + )} @@ -557,4 +561,7 @@ const StyledLabel = styled.label` input[type='file'] { display: none; } + &:hover { + color: ${COLORS.blue50}; + } ` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx index 7c88d4187d5..823a017cb2c 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectRobot.tsx @@ -21,6 +21,7 @@ export function SelectRobot(props: WizardTileProps): JSX.Element { return ( void subHeader?: string - imgSrc?: string tooltipOnDisabled?: string } + +const OT2_GIFS: Record = { + 2: new URL( + '../../assets/images/onboarding_animation_ot2_2.gif', + import.meta.url + ).href, + 3: new URL( + '../../assets/images/onboarding_animation_ot2_3.gif', + import.meta.url + ).href, + 4: new URL( + '../../assets/images/onboarding_animation_ot2_4.gif', + import.meta.url + ).href, +} + +const ONBOARDING_ANIMATIONS: Record = { + 1: one, + 2: two, + 3: three, + 4: four, + 5: five, + 6: six, +} + export function WizardBody(props: WizardBodyProps): JSX.Element { const { stepNumber, @@ -41,13 +73,27 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { subHeader, proceed, disabled = false, - imgSrc, tooltipOnDisabled, + robotType, } = props const { t } = useTranslation('shared') const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'top', }) + const [asset, setAsset] = useState(null) + const [loaded, setLoaded] = useState(false) + + useLayoutEffect(() => { + const videoAsset = ONBOARDING_ANIMATIONS[stepNumber] + setLoaded(false) + setAsset(videoAsset) + const timeout = setTimeout(() => { + setLoaded(true) + }, 100) + return () => { + clearTimeout(timeout) + } + }, [stepNumber]) return ( - - + + {robotType === FLEX_ROBOT_TYPE || stepNumber === 1 ? ( + + ) : ( + + )} ) } - -const StyledImg = styled.img` - border-radius: ${BORDERS.borderRadius16}; - max-height: 844px; -` diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx index 085e2e76efc..fe33c8266c9 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/WizardBody.test.tsx @@ -2,6 +2,7 @@ import type * as React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { i18n } from '../../../assets/localization' import { renderWithProviders } from '../../../__testing-utils__' import { WizardBody } from '../WizardBody' @@ -24,6 +25,7 @@ describe('WizardBody', () => { disabled: false, goBack: vi.fn(), subHeader: 'mockSubheader', + robotType: FLEX_ROBOT_TYPE, } }) @@ -37,6 +39,6 @@ describe('WizardBody', () => { expect(props.proceed).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(props.goBack).toHaveBeenCalled() - screen.getByRole('img', { name: '' }) + screen.getByLabelText('onboarding animation for page 1') }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index 921e9c1fc55..91e793f5005 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -195,15 +195,14 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { ) return ( - + - - {hoverSlot != null && - breakPointSize !== 'small' && - LEFT_SLOTS.includes(hoverSlot) ? ( - - ) : null} - + {zoomIn.slot == null ? ( + + {hoverSlot != null && + breakPointSize !== 'small' && + LEFT_SLOTS.includes(hoverSlot) ? ( + + ) : null} + + ) : null} {() => ( <> @@ -356,13 +357,15 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { )} - - {hoverSlot != null && - breakPointSize !== 'small' && - !LEFT_SLOTS.includes(hoverSlot) ? ( - - ) : null} - + {zoomIn.slot == null ? ( + + {hoverSlot != null && + breakPointSize !== 'small' && + !LEFT_SLOTS.includes(hoverSlot) ? ( + + ) : null} + + ) : null} {zoomIn.slot != null && zoomIn.cutout != null ? ( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 62a5f92d46e..44732c1e0ed 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -18,6 +18,7 @@ import { } from '@opentrons/components' import { FLEX_ROBOT_TYPE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, getModuleDisplayName, getModuleType, MAGNETIC_MODULE_TYPE, @@ -58,7 +59,7 @@ import { LabwareTools } from './LabwareTools' import { MagnetModuleChangeContent } from './MagnetModuleChangeContent' import { getModuleModelsBySlot, getDeckErrors } from './utils' -import type { ModuleModel } from '@opentrons/shared-data' +import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' import type { Fixture } from './constants' @@ -242,7 +243,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { handleResetSearchTerm() } - const handleClear = (): void => { + const handleClear = (keepExistingLabware = false): void => { onDeckProps?.setHoveredModule(null) onDeckProps?.setHoveredFixture(null) if (slot !== 'offDeck') { @@ -250,31 +251,41 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if (createdModuleForSlot != null) { dispatch(deleteModule(createdModuleForSlot.id)) } - // clear fixture(s) from slot - if (createFixtureForSlots != null && createFixtureForSlots.length > 0) { - createFixtureForSlots.forEach(fixture => - dispatch(deleteDeckFixture(fixture.id)) - ) - } // clear labware from slot if ( createdLabwareForSlot != null && - createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri + (!keepExistingLabware || + createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri) ) { dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) } // clear nested labware from slot if ( createdNestedLabwareForSlot != null && - createdNestedLabwareForSlot.labwareDefURI !== - selectedNestedLabwareDefUri + (!keepExistingLabware || + createdNestedLabwareForSlot.labwareDefURI !== + selectedNestedLabwareDefUri) ) { dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id })) } // clear labware on staging area 4th column slot - if (matchingLabwareFor4thColumn != null) { + if (matchingLabwareFor4thColumn != null && !keepExistingLabware) { dispatch(deleteContainer({ labwareId: matchingLabwareFor4thColumn.id })) } + // clear fixture(s) from slot + if (createFixtureForSlots != null && createFixtureForSlots.length > 0) { + createFixtureForSlots.forEach(fixture => + dispatch(deleteDeckFixture(fixture.id)) + ) + // zoom out if you're clearing a staging area slot directly from a 4th column + if ( + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + slot as AddressableAreaName + ) + ) { + dispatch(selectZoomedIntoSlot({ slot: null, cutout: null })) + } + } } handleResetToolbox() handleResetLabwareTools() @@ -285,7 +296,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } const handleConfirm = (): void => { // clear entities first before recreating them - handleClear() + handleClear(true) if (selectedFixture != null && cutout != null) { // create fixture(s) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx index b45f314f689..c6c37c5be31 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx @@ -15,6 +15,12 @@ import { StyledText, useOnClickOutside, } from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, + getCutoutIdFromAddressableArea, + getDeckDefFromRobotType, +} from '@opentrons/shared-data' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { deleteModule } from '../../../step-forms/actions' @@ -32,10 +38,12 @@ import { getStagingAreaAddressableAreas } from '../../../utils' import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors' import type { MouseEvent, SetStateAction } from 'react' import type { + AddressableAreaName, CoordinateTuple, CutoutId, DeckSlotId, } from '@opentrons/shared-data' + import type { LabwareOnDeck } from '../../../step-forms' import type { ThunkDispatch } from '../../../types' @@ -66,6 +74,7 @@ interface SlotOverflowMenuProps { setShowMenuList: (value: SetStateAction) => void addEquipment: (slotId: string) => void menuListSlotPosition?: CoordinateTuple + invertY?: true } export function SlotOverflowMenu( props: SlotOverflowMenuProps @@ -75,6 +84,7 @@ export function SlotOverflowMenu( setShowMenuList, addEquipment, menuListSlotPosition, + invertY = false, } = props const { t } = useTranslation('starting_deck_state') const navigate = useNavigate() @@ -113,9 +123,16 @@ export function SlotOverflowMenu( const isLabwareTiprack = labwareOnSlot?.def.parameters.isTiprack ?? false const isLabwareAnAdapter = labwareOnSlot?.def.allowedRoles?.includes('adapter') ?? false + + const isTiprackAdapter = + labwareOnSlot?.def.parameters.quirks?.includes( + 'tiprackAdapterFor96Channel' + ) ?? false + const nestedLabwareOnSlot = Object.values(deckSetupLabware).find( lw => lw.slot === labwareOnSlot?.id ) + const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( ae => ae.location?.split('cutout')[1] === location ) @@ -137,6 +154,10 @@ export function SlotOverflowMenu( const hasNoItems = moduleOnSlot == null && labwareOnSlot == null && fixturesOnSlot.length === 0 + const isStagingSlot = FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + location as AddressableAreaName + ) + const handleClear = (): void => { // clear module from slot if (moduleOnSlot != null) { @@ -158,6 +179,21 @@ export function SlotOverflowMenu( if (matchingLabware != null) { dispatch(deleteContainer({ labwareId: matchingLabware.id })) } + // delete staging slot if addressable area is on staging slot + if (isStagingSlot) { + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const cutoutId = getCutoutIdFromAddressableArea(location, deckDef) + const stagingAreaEquipmentId = Object.values( + additionalEquipmentOnDeck + ).find(({ location }) => location === cutoutId)?.id + if (stagingAreaEquipmentId != null) { + dispatch(deleteDeckFixture(stagingAreaEquipmentId)) + } else { + console.error( + `could not find equipment id for entity in ${location} with cutout id ${cutoutId}` + ) + } + } } const showDuplicateBtn = @@ -170,8 +206,9 @@ export function SlotOverflowMenu( (labwareOnSlot != null && !isLabwareAnAdapter && !isLabwareTiprack && + !isTiprackAdapter && nestedLabwareOnSlot == null) || - nestedLabwareOnSlot != null + (nestedLabwareOnSlot != null && !isTiprackAdapter) let position = ROBOT_BOTTOM_HALF_SLOTS.includes(location) ? BOTTOM_SLOT_Y_POSITION @@ -293,7 +330,7 @@ export function SlotOverflowMenu( ) : null} { if (matchingLabware != null) { setShowDeleteLabwareModal(true) @@ -325,7 +362,7 @@ export function SlotOverflowMenu( innerDivProps={{ style: { position: POSITION_ABSOLUTE, - transform: 'rotate(180deg) scaleX(-1)', + transform: `rotate(180deg) scaleX(-1) ${invertY ? 'scaleY(-1)' : ''}`, }, }} > diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 5371faed57c..5eab480710e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { @@ -67,6 +67,9 @@ describe('DeckSetupTools', () => { }) vi.mocked(getDismissedHints).mockReturnValue([]) }) + afterEach(() => { + vi.resetAllMocks() + }) it('should render the relevant modules and fixtures for slot D3 on Flex with tabs', () => { render(props) screen.getByText('Add a module') @@ -92,6 +95,14 @@ describe('DeckSetupTools', () => { screen.getByText('mock labware tools') }) it('should clear the slot from all items when the clear cta is called', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: 'mockUri', + selectedNestedLabwareDefUri: 'mockUri', + selectedFixture: null, + selectedModuleModel: null, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + vi.mocked(getDeckSetupForActiveItem).mockReturnValue({ labware: { labId: { diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx index 2ba0d4df60f..56d5af2f806 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx @@ -1,5 +1,5 @@ import type * as React from 'react' -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { fixture96Plate } from '@opentrons/shared-data' @@ -42,6 +42,8 @@ const render = (props: React.ComponentProps) => { })[0] } +const MOCK_STAGING_AREA_ID = 'MOCK_STAGING_AREA_ID' + describe('SlotOverflowMenu', () => { let props: React.ComponentProps @@ -78,7 +80,11 @@ describe('SlotOverflowMenu', () => { }, }, additionalEquipmentOnDeck: { - fixture: { name: 'stagingArea', id: 'mockId', location: 'cutoutD3' }, + fixture: { + name: 'stagingArea', + id: MOCK_STAGING_AREA_ID, + location: 'cutoutD3', + }, }, }) vi.mocked(EditNickNameModal).mockReturnValue( @@ -87,6 +93,10 @@ describe('SlotOverflowMenu', () => { vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({}) }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should renders all buttons as enabled and clicking on them calls ctas', () => { render(props) fireEvent.click( @@ -134,4 +144,25 @@ describe('SlotOverflowMenu', () => { expect(mockNavigate).toHaveBeenCalled() expect(vi.mocked(openIngredientSelector)).toHaveBeenCalled() }) + it('deletes the staging area slot and all labware and modules on top of it', () => { + vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({ + labId2: { well1: { '0': { volume: 10 } } }, + }) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Clear slot' })) + + expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledOnce() + expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledWith( + MOCK_STAGING_AREA_ID + ) + expect(vi.mocked(deleteContainer)).toHaveBeenCalledTimes(2) + expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(1, { + labwareId: 'labId', + }) + expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(2, { + labwareId: 'labId2', + }) + expect(vi.mocked(deleteModule)).toHaveBeenCalledOnce() + expect(vi.mocked(deleteModule)).toHaveBeenCalledWith('modId') + }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index 7a1c7c09be3..a288947365a 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -215,15 +215,15 @@ export function zoomInOnCoordinate(props: ZoomInOnCoordinateProps): string { const { x, y, deckDef } = props const [width, height] = [deckDef.dimensions[0], deckDef.dimensions[1]] - const zoomFactor = 0.6 + const zoomFactor = 0.55 const newWidth = width * zoomFactor const newHeight = height * zoomFactor // +125 and +50 to get the approximate center of the screen point - const newMinX = x - newWidth / 2 + 125 + const newMinX = x - newWidth / 2 + 20 const newMinY = y - newHeight / 2 + 50 - return `${newMinX} ${newMinY} ${newWidth} ${newHeight}` + return `${newMinX} ${newMinY} ${newWidth} ${newHeight + 70}` } export interface AnimateZoomProps { diff --git a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx index 36fb6984a66..f631ee3391a 100644 --- a/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx +++ b/protocol-designer/src/pages/Designer/Offdeck/OffDeckDetails.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + ALIGN_CENTER, BORDERS, COLORS, DIRECTION_COLUMN, @@ -26,6 +27,8 @@ import { SlotOverflowMenu } from '../DeckSetup/SlotOverflowMenu' import type { DeckSlotId } from '@opentrons/shared-data' import type { DeckSetupTabType } from '../types' +const OFFDECK_MAP_WIDTH = '41.625rem' + interface OffDeckDetailsProps extends DeckSetupTabType { addLabware: () => void } @@ -43,19 +46,30 @@ export function OffDeckDetails(props: OffDeckDetailsProps): JSX.Element { const allWellContentsForActiveItem = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem ) + const containerWidth = tab === 'startingDeck' ? '100vw' : '75vh' + const paddingLeftWithHover = + hoverSlot == null + ? `calc((${containerWidth} - (${SPACING.spacing24} * 2) - ${OFFDECK_MAP_WIDTH}) / 2)` + : SPACING.spacing24 + const paddingLeft = tab === 'startingDeck' ? paddingLeftWithHover : undefined + const padding = + tab === 'protocolSteps' + ? SPACING.spacing24 + : `${SPACING.spacing24} ${paddingLeft}` + const stepDetailsContainerWidth = `calc(((${containerWidth} - ${OFFDECK_MAP_WIDTH}) / 2) - (${SPACING.spacing24} * 3))` return ( {hoverSlot != null ? ( - + ) : null} { setShowMenuListForId(null) }} + menuListSlotPosition={[0, 0, 0]} + invertY /> ) : null} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMixTools.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMixTools.tsx index 614fc880d5d..6f39f7ff632 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMixTools.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMixTools.tsx @@ -22,6 +22,7 @@ import { getBlowoutLocationOptionsForForm, getLabwareFieldForPositioningField, } from '../StepForm/utils' + import type { WellOrderOption } from '../../../../form-types' import type { FieldPropsByName } from '../StepForm/types' @@ -59,24 +60,18 @@ export function BatchEditMixTools(props: BatchEditMixToolsProps): JSX.Element { return pipetteId ? String(pipetteId) : null } - const getWellOrderFieldValue = ( - name: string - ): WellOrderOption | null | undefined => { - const val = propsForFields[name]?.value - if (val === 'l2r' || val === 'r2l' || val === 't2b' || val === 'b2t') { - return val - } else { - return null - } - } - return ( - - + + - + @@ -115,7 +116,7 @@ export function BatchEditMixTools(props: BatchEditMixToolsProps): JSX.Element { ) : null} @@ -159,6 +160,7 @@ export function BatchEditMixTools(props: BatchEditMixToolsProps): JSX.Element { options={getBlowoutLocationOptionsForForm({ stepType: 'mix', })} + padding="0" /> ) : null} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMoveLiquidTools.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMoveLiquidTools.tsx index b032bab56b3..561a926cc8f 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMoveLiquidTools.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/BatchEditToolbox/BatchEditMoveLiquidTools.tsx @@ -61,24 +61,18 @@ export function BatchEditMoveLiquidTools( const labwareId = propsForFields[labwareField]?.value return labwareId ? String(labwareId) : null } - const getWellOrderFieldValue = ( - name: string - ): WellOrderOption | null | undefined => { - const val = propsForFields[name]?.value - if (val === 'l2r' || val === 'r2l' || val === 't2b' || val === 'b2t') { - return val - } else { - return null - } - } return ( - + - + @@ -119,7 +113,7 @@ export function BatchEditMoveLiquidTools( @@ -227,6 +221,7 @@ export function BatchEditMoveLiquidTools( path: propsForFields.path.value as any, stepType: 'moveLiquid', })} + padding="0" /> ) : null} { setToolboxStep(0) setShowFormErrors(false) + handleScrollToTop() }} > {i18n.format(t('shared:back'), 'capitalize')} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx index 5d2035620d3..3447de54069 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx @@ -41,7 +41,7 @@ export function HeaterShakerTools(props: StepFormProps): JSX.Element { gridGap={SPACING.spacing4} paddingX={SPACING.spacing16} > - + {t('protocol_steps:heater_shaker_settings')} + + - - {enableReturnTip ? ( <> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts index 1f79fe88440..5b316bc206b 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts @@ -84,6 +84,11 @@ describe('capitalizeFirstLetter', () => { 'Move labware to D3 on top of Magnetic Block' ) }) + + it('should capitalize the first letter of a step name and leave the rest unchanged', () => { + const moduleName = 'Heater-shaker' + expect(capitalizeFirstLetter(moduleName)).toBe('Heater-Shaker') + }) }) describe('getFormErrorsMappedToField', () => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts index 3821d0ba49d..db336a0aba1 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts @@ -337,8 +337,17 @@ export const getSaveStepSnackbarText = ( } } -export const capitalizeFirstLetter = (stepName: string): string => - `${stepName.charAt(0).toUpperCase()}${stepName.slice(1)}` +export const capitalizeFirstLetter = (stepName: string): string => { + // Note - check is for heater-shaker + if (stepName.includes('-')) { + return stepName + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join('-') + } else { + return `${stepName.charAt(0).toUpperCase()}${stepName.slice(1)}` + } +} type ErrorMappedToField = Record diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx index 36c82e86199..f97d73e469f 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx @@ -79,7 +79,6 @@ interface StepSummaryProps { export function StepSummary(props: StepSummaryProps): JSX.Element | null { const { currentStep, stepDetails } = props const { t } = useTranslation(['protocol_steps', 'application']) - const labwareNicknamesById = useSelector(getLabwareNicknamesById) const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities @@ -91,7 +90,6 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { return null } const { stepType } = currentStep - let stepSummaryContent: JSX.Element | null = null switch (stepType) { case 'mix': @@ -184,16 +182,20 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { ) : ( - - + + + + void @@ -39,7 +37,6 @@ export function MultichannelSubstep( stepId, selectSubstep, substepIndex, - ingredNames, trashName, isSameLabware, } = props @@ -107,7 +104,6 @@ export function MultichannelSubstep( trashName={trashName} key={rowKey} volume={row.volume} - ingredNames={ingredNames} source={row.source} dest={row.dest} stepId={stepId} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx index 5c2be0bc9fa..82da4015e45 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/PipettingSubsteps.tsx @@ -4,7 +4,6 @@ import { MultichannelSubstep } from './MultichannelSubstep' import type { SourceDestSubstepItem, SubstepIdentifier, - WellIngredientNames, } from '../../../../steplist' import { useSelector } from 'react-redux' import { @@ -14,13 +13,12 @@ import { interface PipettingSubstepsProps { substeps: SourceDestSubstepItem - ingredNames: WellIngredientNames selectSubstep: (substepIdentifier: SubstepIdentifier) => void hoveredSubstep?: SubstepIdentifier | null } export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { - const { substeps, selectSubstep, hoveredSubstep, ingredNames } = props + const { substeps, selectSubstep, hoveredSubstep } = props const stepId = substeps.parentStepId const formData = useSelector(getSavedStepForms)[stepId] const additionalEquipment = useSelector(getAdditionalEquipment) @@ -52,7 +50,6 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { stepId={substeps.parentStepId} substepIndex={groupKey} selectSubstep={selectSubstep} - ingredNames={ingredNames} isSameLabware={isSameLabware} /> ) @@ -64,7 +61,6 @@ export function PipettingSubsteps(props: PipettingSubstepsProps): JSX.Element { selectSubstep={selectSubstep} stepId={substeps.parentStepId} substepIndex={substepIndex} - ingredNames={ingredNames} volume={row.volume} source={row.source} dest={row.dest} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index 4ed55987f08..9cde39888ce 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -45,8 +45,8 @@ import type { IconName } from '@opentrons/components' import type { StepIdType } from '../../../../form-types' import type { BaseState } from '../../../../types' -const STARTING_DECK_STATE = 'Starting deck state' -const FINAL_DECK_STATE = 'Final deck state' +const STARTING_DECK_STATE = 'Starting deck' +const FINAL_DECK_STATE = 'Ending deck' const PX_HEIGHT_TO_TOP_OF_CONTAINER = 32 export interface StepContainerProps { title: string diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx index 5bb125b3269..3dd7f529743 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepOverflowMenu.tsx @@ -64,7 +64,9 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { const isPipetteStep = savedStepFormData.stepType === 'moveLiquid' || savedStepFormData.stepType === 'mix' - const isThermocyclerProfile = savedStepFormData.stepType === 'thermocycler' + const isThermocyclerProfile = + savedStepFormData.stepType === 'thermocycler' && + savedStepFormData.thermocyclerFormType === 'thermocyclerProfile' const duplicateStep = ( stepId: StepIdType diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx index a34dc799337..7c62f23140d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/Substep.tsx @@ -1,38 +1,24 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import noop from 'lodash/noop' -import { AIR } from '@opentrons/step-generation' import { ALIGN_CENTER, - COLORS, DIRECTION_COLUMN, DeckInfoLabel, Flex, JUSTIFY_SPACE_BETWEEN, - LiquidIcon, ListItem, SPACING, StyledText, Tag, } from '@opentrons/components' -import { selectors } from '../../../../labware-ingred/selectors' -import { - MIXED_WELL_COLOR, - swatchColors, -} from '../../../../components/swatchColors' -import { compactPreIngreds, formatVolume } from './utils' +import { formatVolume } from './utils' import type { AdditionalEquipmentName } from '@opentrons/step-generation' -import type { - SubstepIdentifier, - SubstepWellData, - WellIngredientNames, -} from '../../../../steplist' +import type { SubstepIdentifier, SubstepWellData } from '../../../../steplist' interface SubstepProps { trashName: AdditionalEquipmentName | null - ingredNames: WellIngredientNames stepId: string substepIndex: number volume?: number | string | null @@ -45,7 +31,6 @@ interface SubstepProps { function SubstepComponent(props: SubstepProps): JSX.Element { const { volume, - ingredNames, stepId, substepIndex, source, @@ -54,24 +39,14 @@ function SubstepComponent(props: SubstepProps): JSX.Element { selectSubstep: propSelectSubstep, isSameLabware, } = props - const { t } = useTranslation(['application', 'protocol_steps', 'shared']) - const compactedSourcePreIngreds = source - ? compactPreIngreds(source.preIngreds) - : {} + const { i18n, t } = useTranslation([ + 'application', + 'protocol_steps', + 'shared', + ]) const selectSubstep = propSelectSubstep ?? noop - const ingredIds: string[] = Object.keys(compactedSourcePreIngreds) - const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) - const noColor = ingredIds.filter(id => id !== AIR).length === 0 - let color = MIXED_WELL_COLOR - if (ingredIds.length === 1) { - color = - liquidDisplayColors[Number(ingredIds[0])] ?? swatchColors(ingredIds[0]) - } else if (noColor) { - color = COLORS.transparent - } - const volumeTag = ( - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {t('protocol_steps:mix')} @@ -123,33 +88,27 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:in')} ) : ( <> - - - {ingredIds.length > 0 ? ( - - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} - {source != null ? ( + {source != null ? ( + + {t('protocol_steps:aspirated')} @@ -159,16 +118,19 @@ function SubstepComponent(props: SubstepProps): JSX.Element { {t('protocol_steps:from')} - ) : null} - - - - {dest !== undefined ? ( + + + ) : null} + {dest != null ? ( + - {ingredIds.length > 0 ? ( - - - - {ingredIds.map(groupId => ingredNames[groupId]).join(',')} - - - ) : null} {dest != null || trashName != null ? ( @@ -195,19 +149,20 @@ function SubstepComponent(props: SubstepProps): JSX.Element { ) : null} - ) : null} - + + ) : null} )} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx index 3a1ddff44f0..eb0a0ba835b 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx @@ -8,7 +8,6 @@ import { StyledText, Toolbox, } from '@opentrons/components' -import { selectors as labwareIngredSelectors } from '../../../../labware-ingred/selectors' import { getSubsteps } from '../../../../file-data/selectors' import { getHoveredSubstep } from '../../../../ui/steps' import { @@ -40,7 +39,6 @@ export function SubstepsToolbox( const substeps = useSelector(getSubsteps)[stepId] const formData = useSelector(getSavedStepForms)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) - const ingredNames = useSelector(labwareIngredSelectors.getLiquidNamesById) const highlightSubstep = (payload: SubstepIdentifier): HoverOnSubstepAction => dispatch(hoverOnSubstep(payload)) @@ -60,6 +58,7 @@ export function SubstepsToolbox( substeps.commandCreatorFnName === 'mix')) || substeps.substepType === THERMOCYCLER_PROFILE ? ( } onCloseClick={handleClose} @@ -85,7 +84,6 @@ export function SubstepsToolbox( ) : ( { titlePadding={SPACING.spacing12} childrenPadding={SPACING.spacing12} confirmButton={formData != null ? undefined : } + height="calc(100vh - 6rem)" > { > { }} /> - + ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/utils.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/utils.test.tsx index 1d6c2149c95..2baccd90d6b 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/utils.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/utils.test.tsx @@ -4,7 +4,7 @@ import { capitalizeFirstLetterAfterNumber } from '../utils' describe('capitalizeFirstLetterAfterNumber', () => { it('should capitalize the first letter of a step type', () => { expect(capitalizeFirstLetterAfterNumber('1. heater-shaker')).toBe( - '1. Heater-shaker' + '1. Heater-Shaker' ) expect(capitalizeFirstLetterAfterNumber('22. thermocycler')).toBe( '22. Thermocycler' diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts index c7f6f812dc2..2d918b7790f 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/utils.ts @@ -1,14 +1,18 @@ import round from 'lodash/round' -import omitBy from 'lodash/omitBy' import uniq from 'lodash/uniq' import { UAParser } from 'ua-parser-js' -import type { WellIngredientVolumeData } from '../../../../steplist' import type { StepIdType } from '../../../../form-types' export const capitalizeFirstLetterAfterNumber = (title: string): string => title.replace( - /(^[\d\W]*)([a-zA-Z])/, - (match, prefix, firstLetter) => `${prefix}${firstLetter.toUpperCase()}` + /(^[\d\W]*)([a-zA-Z])|(-[a-zA-Z])/g, + (match, prefix, firstLetter) => { + if (prefix) { + return `${prefix}${firstLetter.toUpperCase()}` + } else { + return `${match.charAt(0)}${match.charAt(1).toUpperCase()}` + } + } ) const VOLUME_SIG_DIGITS_DEFAULT = 2 @@ -29,31 +33,6 @@ export const formatPercentage = (part: number, total: number): string => { return `${round((part / total) * 100, PERCENTAGE_DECIMALS_ALLOWED)}%` } -export const compactPreIngreds = ( - preIngreds: WellIngredientVolumeData -): Partial< - | { - [ingredId: string]: - | { - volume: number - } - | undefined - } - | { - [well: string]: - | { - [ingredId: string]: { - volume: number - } - } - | undefined - } -> => { - return omitBy(preIngreds, ingred => { - return typeof ingred?.volume === 'number' && ingred.volume <= 0 - }) -} - export const getMetaSelectedSteps = ( multiSelectItemIds: StepIdType[] | null, stepId: StepIdType, diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx index f68928c3488..6984d07e8b9 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/__tests__/ProtocolSteps.test.tsx @@ -10,8 +10,12 @@ import { import { getSelectedStepId, getSelectedSubstep, + getSelectedTerminalItemId, } from '../../../../ui/steps/selectors' -import { getDesignerTab } from '../../../../file-data/selectors' +import { + getDesignerTab, + getRobotStateTimeline, +} from '../../../../file-data/selectors' import { getEnableHotKeysDisplay } from '../../../../feature-flags/selectors' import { DeckSetupContainer } from '../../DeckSetup' import { OffDeck } from '../../Offdeck' @@ -56,10 +60,15 @@ const MOCK_STEP_FORMS = { describe('ProtocolSteps', () => { beforeEach(() => { vi.mocked(getDesignerTab).mockReturnValue('protocolSteps') + vi.mocked(getRobotStateTimeline).mockReturnValue({ + timeline: [], + errors: [], + }) vi.mocked(TimelineToolbox).mockReturnValue(
mock TimelineToolbox
) vi.mocked(DeckSetupContainer).mockReturnValue(
mock DeckSetupContainer
) + vi.mocked(getSelectedTerminalItemId).mockReturnValue(null) vi.mocked(OffDeck).mockReturnValue(
mock OffDeck
) vi.mocked(getUnsavedForm).mockReturnValue(null) vi.mocked(getSelectedSubstep).mockReturnValue(null) @@ -95,8 +104,8 @@ describe('ProtocolSteps', () => { it('renders the hot keys display', () => { render() screen.getByText('Double-click to edit') - screen.getByText('Shift + Click to select all') - screen.getByText('Command + Click for multi-select') + screen.getByText('Shift + click to select range') + screen.getByText('Command + click to select multiple') }) it('renders the current step name', () => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index b5e5810c2da..4617163a247 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -3,13 +3,11 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - Box, COLORS, DIRECTION_COLUMN, + FLEX_MAX_CONTENT, Flex, JUSTIFY_CENTER, - JUSTIFY_FLEX_END, - JUSTIFY_FLEX_START, JUSTIFY_SPACE_BETWEEN, POSITION_FIXED, SPACING, @@ -27,6 +25,8 @@ import { getSelectedSubstep, getSelectedStepId, getHoveredStepId, + getSelectedTerminalItemId, + getHoveredTerminalItemId, } from '../../../ui/steps/selectors' import { DeckSetupContainer } from '../DeckSetup' import { OffDeck } from '../Offdeck' @@ -34,7 +34,10 @@ import { TimelineToolbox, SubstepsToolbox } from './Timeline' import { StepForm } from './StepForm' import { StepSummary } from './StepSummary' import { BatchEditToolbox } from './BatchEditToolbox' -import { getDesignerTab } from '../../../file-data/selectors' +import { + getDesignerTab, + getRobotStateTimeline, +} from '../../../file-data/selectors' import { TimelineAlerts } from '../../../organisms' const CONTENT_MAX_WIDTH = '46.9375rem' @@ -42,6 +45,8 @@ const CONTENT_MAX_WIDTH = '46.9375rem' export function ProtocolSteps(): JSX.Element { const { i18n, t } = useTranslation('starting_deck_state') const formData = useSelector(getUnsavedForm) + const selectedTerminalItem = useSelector(getSelectedTerminalItemId) + const hoveredTerminalItem = useSelector(getHoveredTerminalItemId) const isMultiSelectMode = useSelector(getIsMultiSelectMode) const selectedSubstep = useSelector(getSelectedSubstep) const enableHoyKeyDisplay = useSelector(getEnableHotKeysDisplay) @@ -62,43 +67,52 @@ export function ProtocolSteps(): JSX.Element { ? savedStepForms[currentstepIdForStepSummary] : null + const { errors: timelineErrors } = useSelector(getRobotStateTimeline) + const hasTimelineErrors = + timelineErrors != null ? timelineErrors.length > 0 : false + const showTimelineAlerts = + hasTimelineErrors && tab === 'protocolSteps' && formData == null const stepDetails = currentStep?.stepDetails ?? null + return ( - {tab === 'protocolSteps' ? ( + {showTimelineAlerts ? ( ) : null} - - {currentStep != null ? ( + + {currentStep != null && hoveredTerminalItem == null ? ( {i18n.format(currentStep.stepName, 'capitalize')} ) : null} + {(hoveredTerminalItem != null || selectedTerminalItem != null) && + currentHoveredStepId == null ? ( + + {t(hoveredTerminalItem ?? selectedTerminalItem)} + + ) : null} + {enableHoyKeyDisplay ? ( - - - - - - - + + + + + ) : null} {formData == null && selectedSubstep ? ( diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx index 8993d271420..f9f343735d4 100644 --- a/protocol-designer/src/pages/Designer/index.tsx +++ b/protocol-designer/src/pages/Designer/index.tsx @@ -6,6 +6,7 @@ import { ALIGN_END, COLORS, DIRECTION_COLUMN, + FLEX_MAX_CONTENT, Flex, INFO_TOAST, SPACING, @@ -149,7 +150,7 @@ export function Designer(): JSX.Element { }} /> ) : null} - + - + {zoomIn.slot == null ? ( { it('renders the settings page without the dev ffs visible', () => { render() screen.getByText('Settings') - screen.getByText('App settings') + screen.getByText('App Info') screen.getByText('Protocol designer version') screen.getByText('fake_PD_version') screen.getAllByText('Release notes') - screen.getByText('User settings') + screen.getByText('User Settings') screen.getByText('Hints') - screen.getByText('Reset all hints and tips notifications') - screen.getByText('Timeline editing tips') + screen.getByText('Show all hints and tips notifications again') + screen.getByText('Timeline editing guidance') screen.getByText( - 'Show tips for working with steps next to the protocol timeline' + 'Show information about working with steps next to the protocol timeline' ) - screen.getByText('Reset hints') + screen.getByText('Reset') screen.getByText('Privacy') - screen.getByText('Share sessions with Opentrons') + screen.getByText('Share analytics with Opentrons') screen.debug() - screen.getByRole('link', { name: 'privacy policy' }) - screen.getByRole('link', { name: 'EULA' }) - screen.getByRole('link', { name: 'Software manual' }) }) it('renders the announcement modal when view release notes button is clicked', () => { vi.mocked(AnnouncementModal).mockReturnValue( @@ -70,7 +67,7 @@ describe('Settings', () => { }) it('renders the hints button and calls to dismiss them when text is pressed', () => { render() - fireEvent.click(screen.getByText('Reset hints')) + fireEvent.click(screen.getByText('Reset')) expect(vi.mocked(clearAllHintDismissals)).toHaveBeenCalled() }) it('renders the analytics toggle and calls the action when pressed', () => { @@ -85,7 +82,7 @@ describe('Settings', () => { }) render() - screen.getByText('Developer feature flags') + screen.getByText('Developer Feature Flags') screen.getByText('Use prerelease mode') screen.getByText('Show in-progress features for testing & internal use') screen.getByText('Disable module placement restrictions') diff --git a/protocol-designer/src/pages/Settings/index.tsx b/protocol-designer/src/pages/Settings/index.tsx index 32669c3bd60..b678327adb8 100644 --- a/protocol-designer/src/pages/Settings/index.tsx +++ b/protocol-designer/src/pages/Settings/index.tsx @@ -52,6 +52,7 @@ export function Settings(): JSX.Element { : analyticsActions.optIn const prereleaseModeEnabled = flags.PRERELEASE_MODE === true + const pdVersion = process.env.OT_PD_VERSION const allFlags = Object.keys(flags) as FlagTypes[] @@ -126,7 +127,7 @@ export function Settings(): JSX.Element { - {t('shared:app_settings')} + {t('shared:app_info')} - {process.env.OT_PD_VERSION} + {pdVersion} @@ -185,7 +186,7 @@ export function Settings(): JSX.Element {
- {t('shared:reset_hints_and_tips')} + {t('shared:show_hints_and_tips')}
@@ -202,7 +203,7 @@ export function Settings(): JSX.Element { > {canClearHintDismissals - ? t('shared:reset_hints') + ? t('shared:reset') : t('shared:no_hints_to_restore')} @@ -245,7 +246,7 @@ export function Settings(): JSX.Element { > - {t('shared:shared_sessions')} + {t('shared:shared_analytics')} None: """Add a new 'source' column to data_files table.""" - select_data_files = sqlalchemy.select(schema_7.data_files_table).order_by( - sqlite_rowid - ) - insert_new_data = sqlalchemy.insert(schema_7.data_files_table) - for old_row in dest_transaction.execute(select_data_files).all(): - dest_transaction.execute( - insert_new_data, - id=old_row.id, - name=old_row.name, - file_hash=old_row.file_hash, - created_at=old_row.created_at, - source=DataFileSourceSQLEnum.UPLOADED, + dest_transaction.execute( + sqlalchemy.update(schema_7.data_files_table).values( + {"source": DataFileSourceSQLEnum.UPLOADED} ) + ) diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 1e377aec3dd..61a66866bb0 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -19,6 +19,7 @@ LoadedModule, LoadedPipette, Liquid, + LiquidClassRecordWithId, ) @@ -185,6 +186,10 @@ class CompletedAnalysis(BaseModel): default_factory=list, description="Liquids used by the protocol", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + default_factory=list, + description="Liquid classes used by the protocol", + ) errors: List[ErrorOccurrence] = Field( ..., description=( diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 71d170c6581..2f46f7857cb 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedLabware, LoadedModule, Liquid, + LiquidClassRecordWithId, ) from opentrons.protocol_engine.protocol_engine import code_in_error_tree @@ -152,6 +153,7 @@ async def update( pipettes: List[LoadedPipette], errors: List[ErrorOccurrence], liquids: List[Liquid], + liquidClasses: List[LiquidClassRecordWithId], ) -> None: """Promote a pending analysis to completed, adding details of its results. @@ -167,6 +169,7 @@ async def update( errors: See `CompletedAnalysis.errors`. Also used to infer whether the completed analysis result is `OK` or `NOT_OK`. liquids: See `CompletedAnalysis.liquids`. + liquidClasses: See `CompletedAnalysis.liquidClasses`. robot_type: See `CompletedAnalysis.robotType`. """ protocol_id = self._pending_store.get_protocol_id(analysis_id=analysis_id) @@ -201,6 +204,7 @@ async def update( pipettes=pipettes, errors=errors, liquids=liquids, + liquidClasses=liquidClasses, ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, @@ -241,6 +245,7 @@ async def save_initialization_failed_analysis( pipettes=[], errors=errors, liquids=[], + liquidClasses=[], ) completed_analysis_resource = CompletedAnalysisResource( id=completed_analysis.id, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index 89387c5cefc..cf1d0687062 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -107,6 +107,7 @@ async def analyze( pipettes=result.state_summary.pipettes, errors=result.state_summary.errors, liquids=result.state_summary.liquids, + liquidClasses=result.state_summary.liquidClasses, ) async def update_to_failed_analysis( @@ -136,6 +137,7 @@ async def update_to_failed_analysis( ) ], liquids=[], + liquidClasses=[], ) def __del__(self) -> None: diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index d77f2326ae7..a57ed636647 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -61,6 +61,7 @@ RunCurrentState, CommandLinkNoMeta, NozzleLayoutConfig, + TipState, ) from ..run_auto_deleter import RunAutoDeleter from ..run_models import Run, BadRun, RunCreate, RunUpdate @@ -590,33 +591,27 @@ async def get_current_state( # noqa: C901 """ try: run = run_data_manager.get(run_id=runId) - active_nozzle_maps = run_data_manager.get_nozzle_maps(run_id=runId) - - nozzle_layouts = { - pipetteId: ActiveNozzleLayout.construct( - startingNozzle=nozzle_map.starting_nozzle, - activeNozzles=nozzle_map.active_nozzles, - config=NozzleLayoutConfig(nozzle_map.configuration.value.lower()), - ) - for pipetteId, nozzle_map in active_nozzle_maps.items() - } - - run = run_data_manager.get(run_id=runId) - current_command = run_data_manager.get_current_command(run_id=runId) - last_completed_command = run_data_manager.get_last_completed_command( - run_id=runId - ) except RunNotCurrentError as e: raise RunStopped(detail=str(e)).as_error(status.HTTP_409_CONFLICT) - links = CurrentStateLinks.construct( - lastCompleted=CommandLinkNoMeta.construct( - id=last_completed_command.command_id, - href=f"/runs/{runId}/commands/{last_completed_command.command_id}", + active_nozzle_maps = run_data_manager.get_nozzle_maps(run_id=runId) + nozzle_layouts = { + pipetteId: ActiveNozzleLayout.construct( + startingNozzle=nozzle_map.starting_nozzle, + activeNozzles=nozzle_map.active_nozzles, + config=NozzleLayoutConfig(nozzle_map.configuration.value.lower()), ) - if last_completed_command is not None - else None - ) + for pipetteId, nozzle_map in active_nozzle_maps.items() + } + + tip_states = { + pipette_id: TipState.construct(hasTip=has_tip) + for pipette_id, has_tip in run_data_manager.get_tip_attached( + run_id=runId + ).items() + } + + current_command = run_data_manager.get_current_command(run_id=runId) estop_engaged = False place_labware = None @@ -671,11 +666,22 @@ async def get_current_state( # noqa: C901 if place_labware: break + last_completed_command = run_data_manager.get_last_completed_command(run_id=runId) + links = CurrentStateLinks.construct( + lastCompleted=CommandLinkNoMeta.construct( + id=last_completed_command.command_id, + href=f"/runs/{runId}/commands/{last_completed_command.command_id}", + ) + if last_completed_command is not None + else None + ) + return await PydanticResponse.create( content=Body.construct( data=RunCurrentState.construct( estopEngaged=estop_engaged, activeNozzleLayouts=nozzle_layouts, + tipStates=tip_states, placeLabwareState=place_labware, ), links=links, diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 66fad61b351..f5a06fa8172 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,7 +1,7 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import List, Optional, Callable, Union, Mapping +from typing import Dict, List, Optional, Callable, Union, Mapping from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -65,6 +65,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + liquidClasses=state_summary.liquidClasses, outputFileIds=state_summary.files, runTimeParameters=run_time_parameters, ) @@ -79,6 +80,7 @@ def _build_run( pipettes=[], modules=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -123,6 +125,7 @@ def _build_run( completedAt=state.completedAt, startedAt=state.startedAt, liquids=state.liquids, + liquidClasses=state.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=state.files, hasEverEnteredErrorRecovery=state.hasEverEnteredErrorRecovery, @@ -511,6 +514,13 @@ def get_nozzle_maps(self, run_id: str) -> Mapping[str, NozzleMapInterface]: raise RunNotCurrentError() + def get_tip_attached(self, run_id: str) -> Dict[str, bool]: + """Get current tip attached states, keyed by pipette id.""" + if run_id == self._run_orchestrator_store.current_run_id: + return self._run_orchestrator_store.get_tip_attached() + + raise RunNotCurrentError() + def get_all_commands_as_preserialized_list( self, run_id: str, include_fixit_commands: bool ) -> List[str]: diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 2ed77b0d0bc..4d5da7560c0 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ LabwareOffset, LabwareOffsetCreate, Liquid, + LiquidClassRecordWithId, CommandNote, ) from opentrons.protocol_engine.types import ( @@ -134,6 +135,10 @@ class Run(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", @@ -215,6 +220,10 @@ class BadRun(ResourceModel): ..., description="Liquids loaded to the run.", ) + liquidClasses: List[LiquidClassRecordWithId] = Field( + ..., + description="Liquid classes loaded to the run.", + ) labwareOffsets: List[LabwareOffset] = Field( ..., description="Labware offsets to apply as labware are loaded.", @@ -316,6 +325,16 @@ class ActiveNozzleLayout(BaseModel): ) +class TipState(BaseModel): + """Information about the tip, if any, currently attached to a pipette.""" + + hasTip: bool + + # todo(mm, 2024-11-15): I think the frontend is currently scraping the commands + # list to figure out where the current tip came from. Extend this class with that + # information so the frontend doesn't have to do that. + + class PlaceLabwareState(BaseModel): """Details the labware being placed by the gripper.""" @@ -331,9 +350,21 @@ class PlaceLabwareState(BaseModel): class RunCurrentState(BaseModel): """Current details about a run.""" - estopEngaged: bool = Field(..., description="") - activeNozzleLayouts: Dict[str, ActiveNozzleLayout] = Field(...) - placeLabwareState: Optional[PlaceLabwareState] = Field(None) + # todo(mm, 2024-11-15): Having estopEngaged here is a bit of an odd man out because + # it's sensor state that can change on its own at any time, whereas the rest of + # these fields are logical state that changes only when commands are run. + # + # Our current mechanism for anchoring these fields to a specific point in time + # (important for avoiding torn-read problems when a client combines this info with + # info from other endpoints) is `links.currentCommand`, which is based on the idea + # that these fields only change when the current command changes. + # + # We should see if clients can replace this with `GET /robot/control/estopStatus`. + estopEngaged: bool + + activeNozzleLayouts: Dict[str, ActiveNozzleLayout] + tipStates: Dict[str, TipState] + placeLabwareState: Optional[PlaceLabwareState] class CommandLinkNoMeta(BaseModel): diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index d43f9f69254..a8ad429db4a 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import List, Optional, Callable, Mapping +from typing import Dict, List, Optional, Callable, Mapping from opentrons.types import NozzleMapInterface from opentrons.protocol_engine.errors.exceptions import EStopActivatedError @@ -298,6 +298,9 @@ async def clear(self) -> RunResult: parameters=run_time_parameters, ) + # todo(mm, 2024-11-15): Are all of these pass-through methods helpful? + # Can we delete them and make callers just call .run_orchestrator.play(), etc.? + def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None: """Start or resume the run.""" self.run_orchestrator.play(deck_configuration=deck_configuration) @@ -334,6 +337,10 @@ def get_nozzle_maps(self) -> Mapping[str, NozzleMapInterface]: """Get the current nozzle map keyed by pipette id.""" return self.run_orchestrator.get_nozzle_maps() + def get_tip_attached(self) -> Dict[str, bool]: + """Get current tip state keyed by pipette id.""" + return self.run_orchestrator.get_tip_attached() + def get_run_time_parameters(self) -> List[RunTimeParameter]: """Parameter definitions defined by protocol, if any. Will always be empty before execution.""" return self.run_orchestrator.get_run_time_parameters() diff --git a/robot-server/tests/data_files/test_data_files_store.py b/robot-server/tests/data_files/test_data_files_store.py index 581577d0a16..9a9b722e6ec 100644 --- a/robot-server/tests/data_files/test_data_files_store.py +++ b/robot-server/tests/data_files/test_data_files_store.py @@ -99,6 +99,7 @@ def _get_sample_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index 717280a6703..516221c500c 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -590,6 +590,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv6 protocol, with liquids 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 35801f8719a..022c86da35e 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 @@ -623,6 +623,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index f85e307e961..961a9a26601 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -626,6 +626,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] --- test_name: Upload and analyze a JSONv8 protocol, with liquids 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 fd98c29a2dc..580688e6e65 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 @@ -51,6 +51,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] runTimeParameters: [] outputFileIds: [] protocolId: '{protocol_id}' 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 3ab7386ba4f..1caf41fbfd1 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 @@ -53,6 +53,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + liquidClasses: [] protocolId: '{protocol_id}' - name: Execute a setup command 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 2ad0a92eb8c..732726d39e9 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 @@ -47,6 +47,7 @@ stages: outputFileIds: [] protocolId: '{protocol_id}' liquids: [] + liquidClasses: [] save: json: original_run_data: data @@ -240,6 +241,7 @@ stages: createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" startedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] completedAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" 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 14ae502d800..95f5077c30e 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 @@ -95,6 +95,7 @@ stages: labware: [] labwareOffsets: [] liquids: [] + liquidClasses: [] runTimeParameters: [] outputFileIds: [] modules: [] 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 index 1f44f7101c7..505ff2d8831 100644 --- 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 @@ -118,6 +118,7 @@ stages: name: sample_plates.csv outputFileIds: [] liquids: [] + liquidClasses: [] protocolId: '{protocol_id}' - name: Play the run 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 35fb6da06c1..29a9c81a3b7 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -75,6 +75,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -150,6 +151,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -200,6 +202,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) @@ -226,6 +229,7 @@ async def test_get_current_run( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) decoy.when(mock_maintenance_run_data_manager.current_run_id).then_return( diff --git a/robot-server/tests/maintenance_runs/router/test_labware_router.py b/robot-server/tests/maintenance_runs/router/test_labware_router.py index d8a8fdab603..4e5ae1152f2 100644 --- a/robot-server/tests/maintenance_runs/router/test_labware_router.py +++ b/robot-server/tests/maintenance_runs/router/test_labware_router.py @@ -38,6 +38,7 @@ def run() -> MaintenanceRun: modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], hasEverEnteredErrorRecovery=False, ) 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 a4431f7b463..07bc9c2e476 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -69,6 +69,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -140,6 +141,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -193,6 +195,7 @@ async def test_create_with_options( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) @@ -262,6 +265,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, ) assert subject.current_run_id == run_id diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 1200f5aff43..d15e9925a18 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -203,6 +203,7 @@ async def test_returned_in_order_added( commands=[], errors=[], liquids=[], + liquidClasses=[], ) subject.add_pending( @@ -266,6 +267,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) result = await subject.get("analysis-id") @@ -283,6 +285,7 @@ async def test_update_adds_details_and_completes_analysis( commands=[], errors=[], liquids=[], + liquidClasses=[], ) assert await subject.get_by_protocol("protocol-id") == [result] assert json.loads(result_as_document) == { @@ -315,6 +318,7 @@ async def test_update_adds_details_and_completes_analysis( "commands": [], "errors": [], "liquids": [], + "liquidClasses": [], "modules": [], } @@ -364,6 +368,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -384,6 +389,7 @@ async def test_update_adds_rtp_values_to_completed_store( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.verify( await mock_completed_store.make_room_and_add( @@ -487,6 +493,7 @@ async def test_update_infers_status_from_errors( modules=[], pipettes=[], liquids=[], + liquidClasses=[], ) analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) @@ -528,6 +535,7 @@ async def test_save_initialization_failed_analysis( commands=[], errors=[error_occurence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 42c12565c14..a8112cdda16 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -209,6 +209,7 @@ async def test_get_by_analysis_id_as_document( "errors": [], "labware": [], "liquids": [], + "liquidClasses": [], "modules": [], "pipettes": [], } diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index 5d3d9da8a13..3fab95879fe 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -189,6 +189,7 @@ async def test_analyze( modules=[], labwareOffsets=[], liquids=[], + liquidClasses=[], wells=[], files=[], hasEverEnteredErrorRecovery=False, @@ -211,6 +212,7 @@ async def test_analyze( pipettes=[analysis_pipette], errors=[], liquids=[], + liquidClasses=[], ) ) @@ -294,5 +296,6 @@ async def test_analyze_updates_pending_on_error( pipettes=[], errors=[error_occurrence], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index ca965d471a8..499bf480cf0 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -526,6 +526,7 @@ def get_completed_analysis_resource( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) @@ -566,6 +567,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) analysis_resource2 = CompletedAnalysisResource( @@ -582,6 +584,7 @@ async def test_get_referenced_data_files( commands=[], errors=[], liquids=[], + liquidClasses=[], ), ) diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index 637a2ee082f..0ae2c591ebd 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1495,6 +1495,7 @@ async def test_get_protocol_analyses( commands=[], errors=[], liquids=[], + liquidClasses=[], ) decoy.when(protocol_store.has("protocol-id")).then_return(True) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 8e9b8d35372..bb7f723138f 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -1,6 +1,4 @@ """Tests for base /runs routes.""" -from typing import Dict - from opentrons.hardware_control import HardwareControlAPI from opentrons_shared_data.robot.types import RobotTypeEnum import pytest @@ -53,6 +51,7 @@ ActiveNozzleLayout, CommandLinkNoMeta, NozzleLayoutConfig, + TipState, ) from robot_server.runs.run_orchestrator_store import RunConflictError from robot_server.runs.run_data_manager import ( @@ -112,23 +111,6 @@ def labware_offset_create() -> LabwareOffsetCreate: ) -@pytest.fixture -def mock_nozzle_maps() -> Dict[str, NozzleMap]: - """Get mock NozzleMaps.""" - return { - "mock-pipette-id": NozzleMap( - configuration=NozzleConfigurationType.FULL, - columns={"1": ["A1"]}, - rows={"A": ["A1"]}, - map_store={"A1": Point(0, 0, 0)}, - starting_nozzle="A1", - valid_map_key="mock-key", - full_instrument_map_store={}, - full_instrument_rows={}, - ) - } - - async def test_create_run( decoy: Decoy, mock_run_data_manager: RunDataManager, @@ -157,6 +139,7 @@ async def test_create_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -245,6 +228,7 @@ async def test_create_protocol_run( labwareOffsets=[], status=pe_types.EngineStatus.IDLE, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -413,6 +397,7 @@ async def test_get_run_data_from_url( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -461,6 +446,7 @@ async def test_get_run() -> None: labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -508,6 +494,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -525,6 +512,7 @@ async def test_get_runs_not_empty( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -605,6 +593,7 @@ async def test_update_run_to_not_current( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -641,6 +630,7 @@ async def test_update_current_none_noop( labware=[], labwareOffsets=[], liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) @@ -867,7 +857,6 @@ async def test_get_current_state_success( decoy: Decoy, mock_run_data_manager: RunDataManager, mock_hardware_api: HardwareControlAPI, - mock_nozzle_maps: Dict[str, NozzleMap], ) -> None: """It should return different state from the current run. @@ -876,8 +865,23 @@ async def test_get_current_state_success( """ run_id = "test-run-id" + decoy.when(mock_run_data_manager.get_tip_attached(run_id=run_id)).then_return( + {"mock-pipette-id": True} + ) + decoy.when(mock_run_data_manager.get_nozzle_maps(run_id=run_id)).then_return( - mock_nozzle_maps + { + "mock-pipette-id": NozzleMap( + configuration=NozzleConfigurationType.FULL, + columns={"1": ["A1"]}, + rows={"A": ["A1"]}, + map_store={"A1": Point(0, 0, 0)}, + starting_nozzle="A1", + valid_map_key="mock-key", + full_instrument_map_store={}, + full_instrument_rows={}, + ) + } ) command_pointer = CommandPointer( command_id="command-id", @@ -909,6 +913,7 @@ async def test_get_current_state_success( config=NozzleLayoutConfig.FULL, ) }, + tipStates={"mock-pipette-id": TipState(hasTip=True)}, ) assert result.content.links == CurrentStateLinks( lastCompleted=CommandLinkNoMeta( @@ -926,7 +931,7 @@ async def test_get_current_state_run_not_current( """It should raise RunStopped when the run is not current.""" run_id = "non-current-run-id" - decoy.when(mock_run_data_manager.get_nozzle_maps(run_id=run_id)).then_raise( + decoy.when(mock_run_data_manager.get(run_id=run_id)).then_raise( RunNotCurrentError("Run is not current") ) diff --git a/robot-server/tests/runs/router/test_labware_router.py b/robot-server/tests/runs/router/test_labware_router.py index 900eac530f1..1252d983efb 100644 --- a/robot-server/tests/runs/router/test_labware_router.py +++ b/robot-server/tests/runs/router/test_labware_router.py @@ -40,6 +40,7 @@ def run() -> Run: labwareOffsets=[], protocolId=None, liquids=[], + liquidClasses=[], outputFileIds=[], hasEverEnteredErrorRecovery=False, ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 751676a6b58..a26baacadbf 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -105,6 +105,7 @@ def engine_state_summary() -> StateSummary: pipettes=[LoadedPipette.construct(id="some-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="some-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) @@ -288,6 +289,7 @@ async def test_create( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=[bool_parameter, file_parameter], outputFileIds=engine_state_summary.files, ) @@ -395,6 +397,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -438,6 +441,7 @@ async def test_get_historical_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -482,6 +486,7 @@ async def test_get_historical_run_no_data( pipettes=[], modules=[], liquids=[], + liquidClasses=[], runTimeParameters=run_time_parameters, outputFileIds=[], ) @@ -503,6 +508,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="current-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], + liquidClasses=[], wells=[], ) current_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -523,6 +529,7 @@ async def test_get_all_runs( pipettes=[LoadedPipette.construct(id="old-pipette-id")], # type: ignore[call-arg] modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], + liquidClasses=[], wells=[], ) historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ @@ -584,6 +591,7 @@ async def test_get_all_runs( pipettes=historical_run_data.pipettes, modules=historical_run_data.modules, liquids=historical_run_data.liquids, + liquidClasses=historical_run_data.liquidClasses, runTimeParameters=historical_run_time_parameters, outputFileIds=historical_run_data.files, ), @@ -601,6 +609,7 @@ async def test_get_all_runs( pipettes=current_run_data.pipettes, modules=current_run_data.modules, liquids=current_run_data.liquids, + liquidClasses=current_run_data.liquidClasses, runTimeParameters=current_run_time_parameters, outputFileIds=current_run_data.files, ), @@ -700,6 +709,7 @@ async def test_update_current( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) @@ -757,6 +767,7 @@ async def test_update_current_noop( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + liquidClasses=engine_state_summary.liquidClasses, runTimeParameters=run_time_parameters, outputFileIds=engine_state_summary.files, ) diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index ce2e5c82da5..aced561bdff 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -4692,7 +4692,7 @@ "type": "object", "properties": { "axes": { - "description": "The axes for which to update the position estimators.", + "description": "The axes for which to update the position estimators. Any axes that are not physically present will be ignored.", "type": "array", "items": { "$ref": "#/definitions/MotorAxis" diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 37e59f9ef54..432e8a08231 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -1940,16 +1940,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2073,16 +2092,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2218,16 +2256,35 @@ "airGapByVolume": { "title": "Airgapbyvolume", "description": "Settings for air gap keyed by target aspiration volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2313,16 +2370,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2339,16 +2415,35 @@ "pushOutByVolume": { "title": "Pushoutbyvolume", "description": "Settings for pushout keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2417,16 +2512,35 @@ "flowRateByVolume": { "title": "Flowratebyvolume", "description": "Settings for flow rate keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2434,16 +2548,35 @@ "conditioningByVolume": { "title": "Conditioningbyvolume", "description": "Settings for conditioning volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2451,16 +2584,35 @@ "disposalByVolume": { "title": "Disposalbyvolume", "description": "Settings for disposal volume keyed by target dispense volume.", - "type": "object", - "additionalProperties": { - "anyOf": [ + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ { - "type": "integer", - "minimum": 0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] }, { - "type": "number", - "minimum": 0.0 + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] } ] } @@ -2690,7 +2842,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", - "p1000_multi_emulsify", + "p1000_multi_em", "p1000_96", "p200_96" ], @@ -5609,7 +5761,7 @@ "type": "object", "properties": { "axes": { - "description": "The axes for which to update the position estimators.", + "description": "The axes for which to update the position estimators. Any axes that are not physically present will be ignored.", "type": "array", "items": { "$ref": "#/definitions/MotorAxis" diff --git a/shared-data/js/__tests__/liquidClassSchema.test.ts b/shared-data/js/__tests__/liquidClassSchema.test.ts new file mode 100644 index 00000000000..75e477637c9 --- /dev/null +++ b/shared-data/js/__tests__/liquidClassSchema.test.ts @@ -0,0 +1,66 @@ +/** Ensure that the liquid class schema itself functions as intended, + * and that all v1 liquid class fixtures will validate */ +import Ajv from 'ajv' +import path from 'path' +import glob from 'glob' +import { describe, expect, it } from 'vitest' +import liquidClassSchemaV1 from '../../liquid-class/schemas/1.json' + +const fixtureV1Glob = path.join( + __dirname, + '../../liquid-class/fixtures/1/*.json' +) +const defV1Glob = path.join( + __dirname, + '../../liquid-class/definitions/1/*.json' +) + +const ajv = new Ajv({ allErrors: true, jsonPointers: true }) + +const validateSchemaV1 = ajv.compile(liquidClassSchemaV1) + +describe('validate v1 liquid class definitions and fixtures', () => { + const fixtures = glob.sync(fixtureV1Glob) + + fixtures.forEach(fixturePath => { + const fixtureDef = require(fixturePath) + + it('fixture validates against schema', () => { + const valid = validateSchemaV1(fixtureDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(fixturePath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) + + const defs = glob.sync(defV1Glob) + + defs.forEach(defPath => { + const liquidClassDef = require(defPath) + + it('liquid class definition validates against v1 schema', () => { + const valid = validateSchemaV1(liquidClassDef) + const validationErrors = validateSchemaV1.errors + + if (validationErrors) { + console.log( + path.parse(defPath).base + + ' ' + + JSON.stringify(validationErrors, null, 4) + ) + } + + expect(validationErrors).toBe(null) + expect(valid).toBe(true) + }) + }) +}) diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 8772a5ab3b9..608bb982887 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -145,6 +145,7 @@ export const OT3_PIPETTES = [ 'p50_single_flex', 'p50_multi_flex', 'p1000_multi_flex', + 'p1000_multi_em_flex', 'p1000_96', 'p200_96', ] @@ -514,6 +515,7 @@ export const SINGLE_RIGHT_SLOT_FIXTURE: 'singleRightSlot' = 'singleRightSlot' export const STAGING_AREA_RIGHT_SLOT_FIXTURE: 'stagingAreaRightSlot' = 'stagingAreaRightSlot' +export const TRASH_BIN_FIXTURE: 'trashBin' = 'trashBin' export const TRASH_BIN_ADAPTER_FIXTURE: 'trashBinAdapter' = 'trashBinAdapter' export const WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: 'wasteChuteRightAdapterCovered' = @@ -590,12 +592,6 @@ export const WASTE_CHUTE_STAGING_AREA_FIXTURES: CutoutFixtureId[] = [ export const LOW_VOLUME_PIPETTES = ['p50_single_flex', 'p50_multi_flex'] -// robot server loads absorbance reader lid as a labware but it is not -// user addressable so we need to hide it where we show labware in the app -export const NON_USER_ADDRESSABLE_LABWARE = [ - 'opentrons_flex_lid_absorbance_plate_reader_module', -] - // default hex values for liquid colors const electricPurple = '#b925ff' const goldenYellow = '#ffd600' diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 4c3fde2c91e..57cb24e31ee 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -10,6 +10,7 @@ import type { RobotType, ThermalAdapterName, } from '../types' +import type { AddressableAreaName, CutoutId } from '../../deck/types/schemaV5' export { getWellNamePerMultiTip } from './getWellNamePerMultiTip' export { getWellTotalVolume } from './getWellTotalVolume' @@ -373,3 +374,28 @@ export const getDeckDefFromRobotType = ( ? standardFlexDeckDef : standardOt2DeckDef } + +export const getCutoutIdFromAddressableArea = ( + addressableAreaName: string, + deckDefinition: DeckDefinition +): CutoutId | null => { + /** + * Given an addressable area name, returns the cutout ID associated with it, or null if there is none + */ + + for (const cutoutFixture of deckDefinition.cutoutFixtures) { + for (const [cutoutId, providedAreas] of Object.entries( + cutoutFixture.providesAddressableAreas + ) as Array<[CutoutId, AddressableAreaName[]]>) { + if (providedAreas.includes(addressableAreaName as AddressableAreaName)) { + return cutoutId + } + } + } + + console.error( + `${addressableAreaName} is not provided by any cutout fixtures in deck definition ${deckDefinition.otId}` + ) + + return null +} diff --git a/shared-data/labware/definitions/2/opentrons_flex_deck_riser/1.json b/shared-data/labware/definitions/2/opentrons_flex_deck_riser/1.json index 59f0548ca32..94533e059b2 100644 --- a/shared-data/labware/definitions/2/opentrons_flex_deck_riser/1.json +++ b/shared-data/labware/definitions/2/opentrons_flex_deck_riser/1.json @@ -13,7 +13,7 @@ "dimensions": { "xDimension": 140, "yDimension": 98, - "zDimension": 21 + "zDimension": 55 }, "wells": {}, "groups": [ diff --git a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json index 9ae49fd8a5e..e86f24c6015 100644 --- a/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json +++ b/shared-data/labware/definitions/2/opentrons_tough_pcr_auto_sealing_lid/1.json @@ -74,7 +74,7 @@ "opentrons_flex_deck_riser": { "x": 0, "y": 0, - "z": 0 + "z": 34 } }, "gripForce": 15, diff --git a/shared-data/liquid-class/definitions/1/water.json b/shared-data/liquid-class/definitions/1/water.json index b5fc2f75486..b84e1676d5b 100644 --- a/shared-data/liquid-class/definitions/1/water.json +++ b/shared-data/liquid-class/definitions/1/water.json @@ -33,12 +33,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -60,12 +59,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -105,12 +103,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -135,9 +132,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -145,14 +140,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -184,12 +178,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -214,21 +207,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[50.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -268,12 +257,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -295,12 +283,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50, - "1": 35, - "10": 24, - "50": 35 - }, + "flowRateByVolume": [ + [1.0, 35.0], + [10.0, 24.0], + [50.0, 35.0] + ], "preWet": false, "mix": { "enable": false, @@ -340,12 +327,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -370,9 +356,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[1.0, 50.0]], "mix": { "enable": false, "params": { @@ -380,14 +364,13 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 2, - "1": 7, - "4.999": 7, - "5": 2, - "10": 2, - "50": 2 - }, + "pushOutByVolume": [ + [1.0, 7.0], + [4.999, 7.0], + [5.0, 2.0], + [10.0, 2.0], + [50.0, 2.0] + ], "delay": { "enable": true, "params": { @@ -419,12 +402,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 0.1, - "1": 0.1, - "49.9": 0.1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 0.1], + [49.9, 0.1], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -449,21 +431,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 50 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 50.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": true, "params": { @@ -503,12 +481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -530,12 +507,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -575,12 +551,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -605,12 +580,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -618,9 +592,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -652,12 +624,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -682,24 +653,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -734,12 +702,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -761,9 +728,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -803,12 +768,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -833,9 +797,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -843,9 +805,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -877,12 +837,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -907,21 +866,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -956,12 +911,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -983,9 +937,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1025,12 +977,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1055,9 +1006,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1065,9 +1014,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1099,12 +1046,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1129,21 +1075,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1183,12 +1125,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1210,12 +1151,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "preWet": false, "mix": { "enable": false, @@ -1255,12 +1195,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1285,12 +1224,11 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], "mix": { "enable": false, "params": { @@ -1298,9 +1236,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1332,12 +1268,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1362,24 +1297,21 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 478, - "5": 318, - "10": 478, - "50": 478 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [ + [5.0, 318.0], + [10.0, 478.0], + [50.0, 478.0] + ], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1414,12 +1346,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1441,9 +1372,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1483,12 +1412,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1513,9 +1441,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1523,9 +1449,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -1557,12 +1481,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -1587,21 +1510,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1636,12 +1555,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1663,9 +1581,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "preWet": false, "mix": { "enable": false, @@ -1705,12 +1621,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1735,9 +1650,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, + "flowRateByVolume": [[1.0, 716.0]], "mix": { "enable": false, "params": { @@ -1745,9 +1658,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -1779,12 +1690,11 @@ "z": 2 }, "speed": 50, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -1809,21 +1719,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 716 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 716.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -1863,12 +1769,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -1890,9 +1795,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -1932,12 +1835,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -1962,9 +1864,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -1972,9 +1872,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2006,12 +1904,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 1, - "1": 1, - "49": 1, - "50": 0 - }, + "airGapByVolume": [ + [1.0, 1.0], + [49.0, 1.0], + [50.0, 0.0] + ], "blowout": { "enable": false }, @@ -2036,21 +1933,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "45": 5, - "50": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [45.0, 5.0], + [50.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2085,12 +1978,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2112,9 +2004,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2154,12 +2044,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2184,9 +2073,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2194,9 +2081,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 15 - }, + "pushOutByVolume": [[1.0, 15.0]], "delay": { "enable": false, "params": { @@ -2228,12 +2113,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "airGapByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "blowout": { "enable": false }, @@ -2258,21 +2142,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "195": 5, - "200": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [195.0, 5.0], + [200.0, 0.0] + ], "delay": { "enable": false, "params": { @@ -2307,12 +2187,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "touchTip": { "enable": false, "params": { @@ -2334,9 +2213,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "preWet": false, "mix": { "enable": false, @@ -2376,12 +2253,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2406,9 +2282,7 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, + "flowRateByVolume": [[1.0, 200.0]], "mix": { "enable": false, "params": { @@ -2416,9 +2290,7 @@ "volume": 50 } }, - "pushOutByVolume": { - "default": 20 - }, + "pushOutByVolume": [[1.0, 20.0]], "delay": { "enable": false, "params": { @@ -2450,12 +2322,11 @@ "z": 2 }, "speed": 35, - "airGapByVolume": { - "default": 10, - "10": 10, - "990": 10, - "1000": 0 - }, + "airGapByVolume": [ + [10.0, 10.0], + [990.0, 10.0], + [1000.0, 0.0] + ], "blowout": { "enable": false }, @@ -2480,21 +2351,17 @@ "y": 0, "z": 2 }, - "flowRateByVolume": { - "default": 200 - }, - "conditioningByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, - "disposalByVolume": { - "default": 5, - "1": 5, - "995": 5, - "1000": 0 - }, + "flowRateByVolume": [[1.0, 200.0]], + "conditioningByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], + "disposalByVolume": [ + [1.0, 5.0], + [995.0, 5.0], + [1000.0, 0.0] + ], "delay": { "enable": false, "params": { diff --git a/shared-data/liquid-class/fixtures/fixture_glycerol50.json b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json similarity index 82% rename from shared-data/liquid-class/fixtures/fixture_glycerol50.json rename to shared-data/liquid-class/fixtures/1/fixture_glycerol50.json index 8befe1d6a5b..20fe7b44a3c 100644 --- a/shared-data/liquid-class/fixtures/fixture_glycerol50.json +++ b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json @@ -33,11 +33,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -59,9 +58,7 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50 - }, + "flowRateByVolume": [[10.0, 50.0]], "preWet": true, "mix": { "enable": true, @@ -101,11 +98,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "blowout": { "enable": true, "params": { @@ -134,11 +130,10 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], "mix": { "enable": true, "params": { @@ -146,11 +141,10 @@ "volume": 15 } }, - "pushOutByVolume": { - "default": 5, - "10": 7, - "20": 10 - }, + "pushOutByVolume": [ + [10.0, 7.0], + [20.0, 10.0] + ], "delay": { "enable": true, "params": { @@ -182,11 +176,10 @@ "z": 5 }, "speed": 100, - "airGapByVolume": { - "default": 2, - "5": 3, - "10": 4 - }, + "airGapByVolume": [ + [5.0, 3.0], + [10.0, 4.0] + ], "touchTip": { "enable": true, "params": { @@ -211,19 +204,12 @@ "y": 0, "z": -5 }, - "flowRateByVolume": { - "default": 50, - "10": 40, - "20": 30 - }, - "conditioningByVolume": { - "default": 10, - "5": 5 - }, - "disposalByVolume": { - "default": 2, - "5": 3 - }, + "flowRateByVolume": [ + [10.0, 40.0], + [20.0, 30.0] + ], + "conditioningByVolume": [[5.0, 5.0]], + "disposalByVolume": [[5.0, 3.0]], "delay": { "enable": true, "params": { diff --git a/shared-data/liquid-class/schemas/1.json b/shared-data/liquid-class/schemas/1.json index 1a5eb18d51a..f3aa85a6168 100644 --- a/shared-data/liquid-class/schemas/1.json +++ b/shared-data/liquid-class/schemas/1.json @@ -90,59 +90,59 @@ "additionalProperties": false }, "airGapByVolume": { - "type": "object", + "type": "array", "description": "Settings for air gap keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "flowRateByVolume": { - "type": "object", + "type": "array", "description": "Settings for flow rate keyed by target aspiration/dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "pushOutByVolume": { - "type": "object", + "type": "array", "description": "Settings for pushout keyed by target aspiration volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "disposalByVolume": { - "type": "object", - "description": "Settings for disposal volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "type": "array", + "description": "An array of two tuples containing positive numbers.", + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "conditioningByVolume": { - "type": "object", + "type": "array", "description": "Settings for conditioning volume keyed by target dispense volume.", - "properties": { - "default": { "$ref": "#/definitions/positiveNumber" } - }, - "patternProperties": { - "d+": { "$ref": "#/definitions/positiveNumber" } + "items": { + "type": "array", + "items": { "$ref": "#/definitions/positiveNumber" }, + "minItems": 2, + "maxItems": 2 }, - "required": ["default"] + "minItems": 1 }, "mix": { "type": "object", @@ -409,7 +409,6 @@ "positionReference", "offset", "flowRateByVolume", - "mix", "conditioningByVolume", "disposalByVolume", "delay" diff --git a/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json similarity index 99% rename from shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json rename to shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json index 0d68704a00a..c267504b404 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel_emulsify/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "FLEX 8-Channel Emulsifying 1000 μL", + "displayName": "FLEX 8-Channel EM 1000 μL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { @@ -312,7 +312,7 @@ "shaftDiameter": 4.5, "shaftULperMM": 15.904, "backlashDistance": 0.1, - "quirks": [], + "quirks": ["highSpeed"], "plungerHomingConfigurations": { "current": 1.0, "speed": 30 diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json similarity index 93% rename from shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json rename to shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json index d464cd5b9fe..b92e7415fe3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", - "pathTo3D": "pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf", + "pathTo3D": "pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], "pipetteBoundingBoxOffsets": { "backLeftCorner": [-38.5, 0.0, -259.15], diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf b/shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf similarity index 100% rename from shared-data/pipette/definitions/2/geometry/eight_channel_emulsify/p1000/placeholder.gltf rename to shared-data/pipette/definitions/2/geometry/eight_channel_em/p1000/placeholder.gltf diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json similarity index 98% rename from shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json rename to shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json index 95292a3f98b..52c7b58171d 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel_emulsify/p1000/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel_em/p1000/default/3_0.json @@ -2,7 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { - "uiMaxFlowRate": 802.9, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -83,7 +83,7 @@ "defaultPushOutVolume": 7 }, "t200": { - "uiMaxFlowRate": 847.9, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -162,7 +162,7 @@ "defaultPushOutVolume": 5 }, "t1000": { - "uiMaxFlowRate": 744.6, + "uiMaxFlowRate": 1431.0, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py index 0462ac5c0e4..62add6a32b0 100644 --- a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py +++ b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py @@ -1,7 +1,7 @@ """Python shared data models for liquid class definitions.""" from enum import Enum -from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence +from typing import TYPE_CHECKING, Literal, Union, Optional, Dict, Any, Sequence, Tuple from pydantic import ( BaseModel, @@ -28,8 +28,8 @@ _NonNegativeNumber = Union[_StrictNonNegativeInt, _StrictNonNegativeFloat] """Non-negative JSON number type, written to preserve lack of decimal point.""" -LiquidHandlingPropertyByVolume = Dict[str, _NonNegativeNumber] -"""Settings for liquid class settings keyed by target aspiration/dispense volume.""" +LiquidHandlingPropertyByVolume = Sequence[Tuple[_NonNegativeNumber, _NonNegativeNumber]] +"""Settings for liquid class settings that are interpolated by volume.""" class PositionReference(Enum): diff --git a/shared-data/python/opentrons_shared_data/pipette/dev_types.py b/shared-data/python/opentrons_shared_data/pipette/dev_types.py index 00676e9be08..8ae367378f2 100644 --- a/shared-data/python/opentrons_shared_data/pipette/dev_types.py +++ b/shared-data/python/opentrons_shared_data/pipette/dev_types.py @@ -31,6 +31,7 @@ "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -57,6 +58,7 @@ class PipetteNameType(str, Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index fb121725c37..40027d54394 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -114,13 +114,13 @@ def load_serial_lookup_table() -> Dict[str, str]: "eight_channel": "M", "single_channel": "S", "ninety_six_channel": "H", - "eight_channel_emulsify": "P", + "eight_channel_em": "P", } _channel_model_str = { "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } _model_shorthand = {"p1000": "p1k", "p300": "p3h"} for channel_dir in _dirs_in(config_path): diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py index c1e03d5ab9d..d72a09e666b 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py @@ -355,7 +355,7 @@ def _update_all_models(configuration_to_update: List[str]) -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/opentrons_shared_data/pipette/types.py b/shared-data/python/opentrons_shared_data/pipette/types.py index 33164904d97..c52e57eb20e 100644 --- a/shared-data/python/opentrons_shared_data/pipette/types.py +++ b/shared-data/python/opentrons_shared_data/pipette/types.py @@ -109,6 +109,7 @@ class Quirks(enum.Enum): dropTipShake = "dropTipShake" doubleDropTip = "doubleDropTip" needsUnstick = "needsUnstick" + highSpeed = "highSpeed" class AvailableUnits(enum.Enum): @@ -216,7 +217,7 @@ def dict_for_encode(self) -> bool: "p1000_single_gen2", "p1000_single_flex", "p1000_multi_flex", - "p1000_multi_emulsify", + "p1000_multi_em", "p1000_96", "p200_96", ] @@ -243,7 +244,7 @@ class PipetteNameType(str, enum.Enum): P1000_SINGLE_GEN2 = "p1000_single_gen2" P1000_SINGLE_FLEX = "p1000_single_flex" P1000_MULTI_FLEX = "p1000_multi_flex" - P1000_MULTI_EMULSIFY = "p1000_multi_emulsify" + P1000_MULTI_EM = "p1000_multi_em" P1000_96 = "p1000_96" P200_96 = "p200_96" diff --git a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py index 3423f0f49e5..774231ac40d 100644 --- a/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py +++ b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py @@ -1,11 +1,46 @@ -from typing import List, Tuple +from typing import List, Tuple, Optional -from opentrons_shared_data.pipette.pipette_definition import PipetteFunctionKeyType +from opentrons_shared_data.pipette.pipette_definition import ( + PipetteFunctionKeyType, + SupportedTipsDefinition, +) +from opentrons_shared_data.pipette.types import UlPerMmAction PIPETTING_FUNCTION_FALLBACK_VERSION: PipetteFunctionKeyType = "1" PIPETTING_FUNCTION_LATEST_VERSION: PipetteFunctionKeyType = "2" +def calculate_ul_per_mm( + ul: float, + action: UlPerMmAction, + active_tip_settings: SupportedTipsDefinition, + requested_pipetting_version: Optional[PipetteFunctionKeyType] = None, + shaft_ul_per_mm: Optional[float] = None, +) -> float: + assumed_requested_pipetting_version = ( + requested_pipetting_version + if requested_pipetting_version + else PIPETTING_FUNCTION_LATEST_VERSION + ) + if action == "aspirate": + fallback = active_tip_settings.aspirate.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.aspirate.default.get( + assumed_requested_pipetting_version, fallback + ) + elif action == "blowout" and shaft_ul_per_mm: + return shaft_ul_per_mm + else: + fallback = active_tip_settings.dispense.default[ + PIPETTING_FUNCTION_FALLBACK_VERSION + ] + sequence = active_tip_settings.dispense.default.get( + assumed_requested_pipetting_version, fallback + ) + return piecewise_volume_conversion(ul, sequence) + + def piecewise_volume_conversion( ul: float, sequence: List[Tuple[float, float, float]] ) -> float: 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 index c5e9cc49604..aae0c1a4e1b 100644 --- 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 @@ -49,7 +49,7 @@ def get_all_pipette_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index 5d3080dbd7a..57f19dfe3ad 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -22,7 +22,7 @@ def iterate_models() -> Iterator[PipetteModel]: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } defn_root = get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" assert os.listdir(defn_root), "A path is wrong" @@ -64,7 +64,7 @@ def test_pick_up_configs_configuration_by_nozzle_map_keys() -> None: "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): @@ -107,7 +107,7 @@ def test_pick_up_configs_configuration_ordered_from_smallest_to_largest() -> Non "single_channel": "single", "ninety_six_channel": "96", "eight_channel": "multi", - "eight_channel_emulsify": "multi_emulsify", + "eight_channel_em": "multi_em", } assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): diff --git a/shared-data/tsconfig-data.json b/shared-data/tsconfig-data.json index 4b9ff960c84..e79657a21f8 100644 --- a/shared-data/tsconfig-data.json +++ b/shared-data/tsconfig-data.json @@ -12,6 +12,7 @@ "deck/**/*.json", "labware/**/*.json", "liquid/**/*.json", + "liquid-class/**/*.json", "command/**/*.json", "commandAnnotation/**/*.json", "gripper/**/*.json", diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index a50e215ee95..57f8970d0c6 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -18,6 +18,7 @@ "command", "errors", "liquid/types", + "liquid-class", "commandAnnotation/types", "vite.config.mts" ] diff --git a/yarn.lock b/yarn.lock index 22621f5ebd4..22c961d6704 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3403,6 +3403,7 @@ reselect "4.0.0" rxjs "^6.5.1" semver "5.7.2" + simple-keyboard-layouts "3.4.41" styled-components "5.3.6" typeface-open-sans "0.0.75" uuid "3.2.1" @@ -20041,6 +20042,11 @@ simple-git@^3.15.1: "@kwsites/promise-deferred" "^1.1.1" debug "^4.3.4" +simple-keyboard-layouts@3.4.41: + version "3.4.41" + resolved "https://registry.yarnpkg.com/simple-keyboard-layouts/-/simple-keyboard-layouts-3.4.41.tgz#eb1504c36626f29b0d5590d419ab39c43d06969a" + integrity sha512-vVnPRgZmK9DqbqUxOgZesdAlWkzY1Cvxf8YaFW3SHJHQKuvCkR8VL6TjJyrpM8BkJa3W4ry1i3CsSydlPckAoQ== + simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"