diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index 8d0658a930e1..738fa369e583 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -112,8 +112,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' - - name: 'downgrade npm version' - run: npm install -g npm@6 - name: check make version run: make --version - name: 'install libudev and libsystemd' @@ -245,8 +243,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' - - name: 'downgrade npm version' - run: npm install -g npm@6 - name: check make version run: make --version - name: 'install libudev and libsystemd' diff --git a/.github/workflows/components-test-build-deploy.yaml b/.github/workflows/components-test-build-deploy.yaml index 78e60426b3f3..6b39fb3b1c82 100644 --- a/.github/workflows/components-test-build-deploy.yaml +++ b/.github/workflows/components-test-build-deploy.yaml @@ -174,10 +174,13 @@ jobs: with: node-version: '18.19.0' registry-url: 'https://registry.npmjs.org' + - name: 'install udev for usb-detection' + run: sudo apt-get update && sudo apt-get install libudev-dev - name: 'setup-js' run: | npm config set cache ./.npm-cache yarn config set cache-folder ./.yarn-cache + make setup-js - name: 'build typescript' run: make build-ts - name: 'build library' diff --git a/.github/workflows/opentrons-ai-client-test-build-deploy.yaml b/.github/workflows/opentrons-ai-client-test-build-deploy.yaml new file mode 100644 index 000000000000..072366ab0d72 --- /dev/null +++ b/.github/workflows/opentrons-ai-client-test-build-deploy.yaml @@ -0,0 +1,78 @@ +# Run tests, build the app, and deploy it cross platform + +name: 'OpentronsAI client test, build, and deploy' + +# ToDo (kk:04/16/2024) Add build and deploy task + +on: + push: + paths: + - 'Makefile' + - 'opentrons-ai-client/**/*' + - 'components/**/*' + - '*.js' + - '*.json' + - 'yarn.lock' + - '.github/workflows/app-test-build-deploy.yaml' + - '.github/workflows/utils.js' + branches: + - '**' + tags: + - 'v*' + - 'ot3@*' + pull_request: + paths: + - 'Makefile' + - 'opentrons-ai-client/**/*' + - 'components/**/*' + - '*.js' + - '*.json' + - 'yarn.lock' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.ref_name != 'edge' || github.run_id}}-${{ github.ref_type != 'tag' || github.run_id }} + cancel-in-progress: true + +env: + CI: true + +jobs: + js-unit-test: + runs-on: 'ubuntu-22.04' + name: 'opentrons ai frontend unit tests' + timeout-minutes: 60 + steps: + - uses: 'actions/checkout@v3' + - uses: 'actions/setup-node@v3' + with: + node-version: '18.19.0' + - name: 'install udev' + run: sudo apt-get update && sudo apt-get install libudev-dev + - name: 'set complex environment variables' + id: 'set-vars' + uses: actions/github-script@v6 + with: + script: | + const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) + buildComplexEnvVars(core, context) + - name: 'cache yarn cache' + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/.npm-cache/_prebuild + ${{ github.workspace }}/.yarn-cache + key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + - name: 'setup-js' + run: | + npm config set cache ${{ github.workspace }}/.npm-cache + yarn config set cache-folder ${{ github.workspace }}/.yarn-cache + make setup-js + - name: 'test frontend packages' + run: | + make -C opentrons-ai-client test-cov + - name: 'Upload coverage report' + uses: codecov/codecov-action@v3 + with: + files: ./coverage/lcov.info + flags: opentrons-ai-client diff --git a/.github/workflows/performance-metrics-test-lint.yaml b/.github/workflows/performance-metrics-test-lint.yaml new file mode 100644 index 000000000000..e57df828caf8 --- /dev/null +++ b/.github/workflows/performance-metrics-test-lint.yaml @@ -0,0 +1,54 @@ +# This workflow runs lint on pull requests that touch anything in the performance-metrics directory + +name: 'performance-metrics test & lint' + +on: + pull_request: + paths: + - 'performance-metrics/**' + - '.github/workflows/performance-metrics-test-lint.yaml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + lint: + name: 'performance-metrics test & lint' + timeout-minutes: 5 + runs-on: 'ubuntu-latest' + steps: + - name: Checkout opentrons repo + uses: 'actions/checkout@v4' + + - name: Setup Python + uses: 'actions/setup-python@v5' + with: + python-version: '3.10' + cache: 'pipenv' + cache-dependency-path: performance-metrics/Pipfile.lock + + - name: "Install Python deps" + uses: './.github/actions/python/setup' + with: + project: 'performance-metrics' + + - name: Setup + id: install + working-directory: ./performance-metrics + run: make setup + + - name: Test + if: always() && steps.install.outcome == 'success' || steps.install.outcome == 'skipped' + working-directory: ./performance-metrics + run: make test + + - name: Lint + if: always() && steps.install.outcome == 'success' || steps.install.outcome == 'skipped' + working-directory: ./performance-metrics + run: make lint diff --git a/.github/workflows/shared-data-test-lint-deploy.yaml b/.github/workflows/shared-data-test-lint-deploy.yaml index 94c56f16a56b..576533371320 100644 --- a/.github/workflows/shared-data-test-lint-deploy.yaml +++ b/.github/workflows/shared-data-test-lint-deploy.yaml @@ -237,7 +237,8 @@ jobs: - name: 'js deps' run: | npm config set cache ./.npm-cache - yarn config set cache-folder ./.yarn-cache + yarn config set cache-folder ./.yarn-cache + make setup-js - name: 'build typescript' run: make build-ts - name: 'build library' diff --git a/.storybook/main.js b/.storybook/main.js index e9fc91cdf48f..985486d5d4e2 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -2,6 +2,7 @@ module.exports = { stories: [ '../components/**/*.stories.@(js|jsx|ts|tsx)', '../app/**/*.stories.@(js|jsx|ts|tsx)', + '../opentrons-ai-client/**/*.stories.@(js|jsx|ts|tsx)', ], addons: [ diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx index d8537e57827a..32864c9abcbc 100644 --- a/.storybook/preview.jsx +++ b/.storybook/preview.jsx @@ -20,7 +20,7 @@ export const parameters = { options: { storySort: { method: 'alphabetical', - order: ['Design Tokens', 'Library', 'App', 'ODD'], + order: ['Design Tokens', 'Library', 'App', 'ODD', 'AI'], }, }, } diff --git a/abr-testing/abr_testing/google_automation/__init__.py b/abr-testing/abr_testing/automation/__init__.py similarity index 100% rename from abr-testing/abr_testing/google_automation/__init__.py rename to abr-testing/abr_testing/automation/__init__.py diff --git a/abr-testing/abr_testing/google_automation/google_drive_tool.py b/abr-testing/abr_testing/automation/google_drive_tool.py similarity index 66% rename from abr-testing/abr_testing/google_automation/google_drive_tool.py rename to abr-testing/abr_testing/automation/google_drive_tool.py index 836ba2083b08..3b65456d0ff5 100644 --- a/abr-testing/abr_testing/google_automation/google_drive_tool.py +++ b/abr-testing/abr_testing/automation/google_drive_tool.py @@ -1,6 +1,8 @@ """Google Drive Tool.""" import os -from typing import Set, Any +from typing import Set, Any, Optional +import webbrowser +import mimetypes from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from googleapiclient.discovery import build from googleapiclient.http import MediaFileUpload @@ -14,15 +16,15 @@ class google_drive: """Google Drive Tool.""" - def __init__(self, credentials: Any, folder_name: str, parent_folder: Any) -> None: + def __init__(self, credentials: Any, folder_name: str, email: str) -> None: """Connects to google drive via credentials file.""" self.scope = ["https://www.googleapis.com/auth/drive"] self.credentials = ServiceAccountCredentials.from_json_keyfile_name( credentials, self.scope ) self.drive_service = build("drive", "v3", credentials=self.credentials) - self.folder_name = folder_name - self.parent_folder = parent_folder + self.parent_folder = folder_name + self.email = email def list_folder(self, delete: Any = False) -> Set[str]: """List folders and files in Google Drive.""" @@ -72,10 +74,9 @@ def upload_file(self, file_path: str) -> str: """Upload file to Google Drive.""" file_metadata = { "name": os.path.basename(file_path), - "mimeType": "application/vnd.google-apps.folder", - "parents": [self.parent_folder] if self.parent_folder else "", + "mimeType": str(mimetypes.guess_type(file_path)[0]), + "parents": [self.parent_folder], } - media = MediaFileUpload(file_path, resumable=True) uploaded_file = ( @@ -83,15 +84,27 @@ def upload_file(self, file_path: str) -> str: .create(body=file_metadata, media_body=media, fields="id") # type: ignore .execute() ) - return uploaded_file["id"] - def upload_missing_files(self, storage_directory: str, missing_files: set) -> None: + def upload_missing_files(self, storage_directory: str) -> None: """Upload missing files to Google Drive.""" + # Read Google Drive .json files. + google_drive_files = self.list_folder() + google_drive_files_json = [ + file for file in google_drive_files if file.endswith(".json") + ] + # Read local directory. + local_files_json = set( + file for file in os.listdir(storage_directory) if file.endswith(".json") + ) + missing_files = local_files_json - set(google_drive_files_json) + print(f"Missing files: {len(missing_files)}") + # Upload missing files. uploaded_files = [] for file in missing_files: file_path = os.path.join(storage_directory, file) uploaded_file_id = google_drive.upload_file(self, file_path) + self.share_permissions(uploaded_file_id) uploaded_files.append( {"name": os.path.basename(file_path), "id": uploaded_file_id} ) @@ -108,3 +121,31 @@ def upload_missing_files(self, storage_directory: str, missing_files: set) -> No print( f"File '{this_name}' was not found in the list of files after uploading." ) + + def open_folder(self) -> Optional[str]: + """Open folder in web browser.""" + folder_metadata = ( + self.drive_service.files() + .get(fileId=self.parent_folder, fields="webViewLink") + .execute() + ) + folder_link = folder_metadata.get("webViewLink") + if folder_link: + print(f"Folder link: {folder_link}") + webbrowser.open( + folder_link + ) # Open the folder link in the default web browser + else: + print("Folder link not found.") + return folder_link + + def share_permissions(self, file_id: str) -> None: + """Share permissions with self.""" + new_permission = { + "type": "user", + "role": "writer", + "emailAddress": self.email, + } + self.drive_service.permissions().create( + fileId=file_id, body=new_permission, transferOwnership=False # type: ignore + ).execute() diff --git a/abr-testing/abr_testing/google_automation/google_sheets_tool.py b/abr-testing/abr_testing/automation/google_sheets_tool.py similarity index 95% rename from abr-testing/abr_testing/google_automation/google_sheets_tool.py rename to abr-testing/abr_testing/automation/google_sheets_tool.py index e486a28fed27..af38a39dcc09 100644 --- a/abr-testing/abr_testing/google_automation/google_sheets_tool.py +++ b/abr-testing/abr_testing/automation/google_sheets_tool.py @@ -2,6 +2,7 @@ import gspread # type: ignore[import] import socket import httplib2 +from datetime import datetime from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import] from typing import Dict, List, Any, Set, Tuple @@ -57,6 +58,12 @@ def write_to_row(self, data: List) -> None: """Write data into a row in a List[] format.""" try: self.row_index += 1 + data = [ + item.strftime("%Y/%m/%d %H:%M:%S") + if isinstance(item, datetime) + else item + for item in data + ] self.worksheet.insert_row(data, index=self.row_index) except socket.gaierror: pass diff --git a/abr-testing/abr_testing/automation/jira_tool.py b/abr-testing/abr_testing/automation/jira_tool.py new file mode 100644 index 000000000000..5c0a2556dfb9 --- /dev/null +++ b/abr-testing/abr_testing/automation/jira_tool.py @@ -0,0 +1,165 @@ +"""JIRA Ticket Creator.""" + +import requests +from requests.auth import HTTPBasicAuth +import json +import webbrowser +import argparse +from typing import List + + +class JiraTicket: + """Connects to JIRA ticket site.""" + + def __init__(self, url: str, api_token: str, email: str) -> None: + """Connect to jira.""" + self.url = url + self.api_token = api_token + self.email = email + self.auth = HTTPBasicAuth(email, api_token) + self.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + + def issues_on_board(self, board_id: str) -> List[str]: + """Print Issues on board.""" + response = requests.get( + f"{self.url}/rest/agile/1.0/board/{board_id}/issue", + headers=self.headers, + auth=self.auth, + ) + response.raise_for_status() + try: + board_data = response.json() + all_issues = board_data["issues"] + except json.JSONDecodeError as e: + print("Error decoding json: ", e) + issue_ids = [] + for i in all_issues: + issue_id = i.get("id") + issue_ids.append(issue_id) + return issue_ids + + def open_issue(self, issue_key: str) -> str: + """Open issue on web browser.""" + url = f"{self.url}/browse/{issue_key}" + print(f"Opening at {url}.") + webbrowser.open(url) + return url + + def create_ticket( + self, + summary: str, + description: str, + project_key: str, + reporter_id: str, + issue_type: str, + priority: str, + components: list, + affects_versions: str, + robot: str, + ) -> str: + """Create ticket.""" + data = { + "fields": { + "project": {"id": "10273", "key": project_key}, + "issuetype": {"name": issue_type}, + "summary": summary, + "reporter": {"id": reporter_id}, + "parent": {"key": robot}, + "priority": {"name": priority}, + "components": [{"name": component} for component in components], + "versions": [{"name": affects_versions}], + "description": { + "content": [ + { + "content": [{"text": description, "type": "text"}], + "type": "paragraph", + } + ], + "type": "doc", + "version": 1, + } + # Include other required fields as needed + } + } + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/", + headers=self.headers, + auth=self.auth, + json=data, + ) + response.raise_for_status() + response_str = str(response.content) + issue_url = response.json().get("self") + issue_key = response.json().get("key") + print(f"issue key {issue_key}") + print(f"issue url{issue_url}") + if issue_key is None: + print("Error: Could not create issue. No key returned.") + except requests.exceptions.HTTPError: + print(f"HTTP error occurred. Response content: {response_str}") + except json.JSONDecodeError: + print(f"JSON decoding error occurred. Response content: {response_str}") + return issue_key + + def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: + """Adds attachments to ticket.""" + # TODO: Ensure that file is actually uploaded. + file = {"file": open(attachment_path, "rb")} + JSON_headers = {"Accept": "application/json"} + try: + response = requests.post( + f"{self.url}/rest/api/3/issue/{issue_id}/attachments", + headers=JSON_headers, + auth=self.auth, + files=file, + ) + print(response) + except json.JSONDecodeError: + error_message = str(response.content) + print(f"JSON decoding error occurred. Response content: {error_message}.") + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = JiraTicket(url, api_token, email) diff --git a/abr-testing/abr_testing/data_collection/abr_calibration_logs.py b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py new file mode 100644 index 000000000000..4d744b5b2f59 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_calibration_logs.py @@ -0,0 +1,224 @@ +"""Get Calibration logs from robots.""" +from typing import Dict, Any, List, Union +import argparse +import os +import json +import gspread # type: ignore[import] +import sys +from abr_testing.data_collection import read_robot_logs +from abr_testing.automation import google_drive_tool, google_sheets_tool + + +def check_for_duplicates( + sheet_location: str, + google_sheet: Any, + col_1: int, + col_2: int, + row: List[str], + headers: List[str], +) -> Union[List[str], None]: + """Check google sheet for duplicates.""" + serials = google_sheet.get_column(col_1) + modify_dates = google_sheet.get_column(col_2) + # check for complete calibration. + if len(row[-1]) > 0: + for serial, modify_date in zip(serials, modify_dates): + if row[col_1 - 1] == serial and row[col_2 - 1] == modify_date: + print(f"Skipped row for instrument {serial}. Already on Google Sheet.") + return None + read_robot_logs.write_to_sheets(sheet_location, google_sheet, row, headers) + print(f"Writing calibration for: {serial}") + return row + + +def upload_calibration_offsets( + calibration: Dict[str, Any], storage_directory: str +) -> None: + """Upload calibration data to google_sheet.""" + # Common Headers + headers_beg = list(calibration.keys())[:4] + headers_end = list(["X", "Y", "Z", "lastModified"]) + # INSTRUMENT SHEET + instrument_headers = ( + headers_beg + list(calibration["Instruments"][0].keys())[:7] + headers_end + ) + local_instrument_file = google_sheet_name + "-Instruments" + instrument_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_instrument_file, instrument_headers + ) + # INSTRUMENTS DATA + instruments = calibration["Instruments"] + for instrument in range(len(instruments)): + one_instrument = instruments[instrument] + x = one_instrument["data"]["calibratedOffset"]["offset"].get("x", "") + y = one_instrument["data"]["calibratedOffset"]["offset"].get("y", "") + z = one_instrument["data"]["calibratedOffset"]["offset"].get("z", "") + modified = one_instrument["data"]["calibratedOffset"].get("last_modified", "") + instrument_row = ( + list(calibration.values())[:4] + + list(one_instrument.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + instrument_sheet_location, + google_sheet_instruments, + 8, + 15, + instrument_row, + instrument_headers, + ) + + # MODULE SHEET + if len(calibration.get("Modules", "")) > 0: + module_headers = ( + headers_beg + list(calibration["Modules"][0].keys())[:7] + headers_end + ) + local_modules_file = google_sheet_name + "-Modules" + modules_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_modules_file, module_headers + ) + # MODULES DATA + modules = calibration["Modules"] + for module in range(len(modules)): + one_module = modules[module] + x = one_module["moduleOffset"]["offset"].get("x", "") + y = one_module["moduleOffset"]["offset"].get("y", "") + z = one_module["moduleOffset"]["offset"].get("z", "") + modified = one_module["moduleOffset"].get("last_modified", "") + module_row = ( + list(calibration.values())[:4] + + list(one_module.values())[:7] + + list([x, y, z, modified]) + ) + check_for_duplicates( + modules_sheet_location, + google_sheet_modules, + 8, + 15, + module_row, + module_headers, + ) + # DECK SHEET + local_deck_file = google_sheet_name + "-Deck" + deck_headers = headers_beg + list(["pipetteCalibratedWith", "Slot"]) + headers_end + deck_sheet_location = read_robot_logs.create_abr_data_sheet( + storage_directory, local_deck_file, deck_headers + ) + # DECK DATA + deck = calibration["Deck"] + slots = ["D3", "D1", "A1"] + deck_modified = deck["data"].get("lastModified", "") + pipette_calibrated_with = deck["data"].get("pipetteCalibratedWith", "") + for i in range(len(deck["data"]["matrix"])): + coords = deck["data"]["matrix"][i] + x = coords[0] + y = coords[1] + z = coords[2] + deck_row = list(calibration.values())[:4] + list( + [pipette_calibrated_with, slots[i], x, y, z, deck_modified] + ) + check_for_duplicates( + deck_sheet_location, google_sheet_deck, 6, 10, deck_row, deck_headers + ) + + +if __name__ == "__main__": + """Get calibration logs.""" + parser = argparse.ArgumentParser( + description="Pulls calibration logs from ABR robots." + ) + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "folder_name", + metavar="FOLDER_NAME", + type=str, + nargs=1, + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "google_sheet_name", + metavar="GOOGLE_SHEET_NAME", + type=str, + nargs=1, + help="Google sheet name.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) + parser.add_argument( + "ip_or_all", + metavar="IP_OR_ALL", + type=str, + nargs=1, + help="Enter 'ALL' to read IPs.json or type full IP address of 1 robot.", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + folder_name = args.folder_name[0] + google_sheet_name = args.google_sheet_name[0] + ip_or_all = args.ip_or_all[0] + email = args.email[0] + # Connect to google drive. + try: + credentials_path = os.path.join(storage_directory, "credentials.json") + except FileNotFoundError: + print(f"Add credentials.json file to: {storage_directory}.") + sys.exit() + try: + google_drive = google_drive_tool.google_drive( + credentials_path, folder_name, email + ) + # Upload calibration logs to google drive. + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # Connect to google sheet + try: + google_sheet_instruments = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + google_sheet_modules = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 1 + ) + google_sheet_deck = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 2 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + ip_json_file = os.path.join(storage_directory, "IPs.json") + try: + ip_file = json.load(open(ip_json_file)) + except FileNotFoundError: + print(f"Add .json file with robot IPs to: {storage_directory}.") + sys.exit() + + if ip_or_all == "ALL": + ip_address_list = ip_file["ip_address_list"] + for ip in ip_address_list: + print(ip) + try: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + except Exception: + print(f"ERROR: Failed to read IP address: {ip}") + continue + else: + saved_file_path, calibration = read_robot_logs.get_calibration_offsets( + ip_or_all, storage_directory + ) + upload_calibration_offsets(calibration, storage_directory) + + google_drive.upload_missing_files(storage_directory) diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py index be3fe1628675..a186019b35b7 100644 --- a/abr-testing/abr_testing/data_collection/abr_google_drive.py +++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py @@ -6,8 +6,8 @@ import gspread # type: ignore[import] from datetime import datetime, timedelta from abr_testing.data_collection import read_robot_logs -from typing import Set, Dict, Any -from abr_testing.google_automation import google_drive_tool, google_sheets_tool +from typing import Set, Dict, Any, Tuple, List, Union +from abr_testing.automation import google_drive_tool, google_sheets_tool def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: @@ -30,8 +30,10 @@ def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]: def create_data_dictionary( - runs_to_save: Set[str], storage_directory: str -) -> Dict[Any, Dict[str, Any]]: + runs_to_save: Union[Set[str], str], + storage_directory: str, + issue_url: str, +) -> Tuple[Dict[Any, Dict[str, Any]], List]: """Pull data from run files and format into a dictionary.""" runs_and_robots = {} for filename in os.listdir(storage_directory): @@ -41,7 +43,9 @@ def create_data_dictionary( file_results = json.load(file) else: continue - run_id = file_results.get("run_id") + if not isinstance(file_results, dict): + continue + run_id = file_results.get("run_id", "NaN") if run_id in runs_to_save: robot = file_results.get("robot_name") protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") @@ -56,6 +60,7 @@ def create_data_dictionary( error_instrument, error_level, ) = read_robot_logs.get_error_info(file_results) + all_modules = get_modules(file_results) start_time_str, complete_time_str, start_date, run_time_min = ( @@ -100,12 +105,25 @@ def create_data_dictionary( "Right Mount": right_pipette, "Extension": extension, } - row_2 = {**row, **all_modules} + tc_dict = read_robot_logs.thermocycler_commands(file_results) + hs_dict = read_robot_logs.hs_commands(file_results) + tm_dict = read_robot_logs.temperature_module_commands(file_results) + notes = {"Note1": "", "Jira Link": issue_url} + row_2 = { + **row, + **all_modules, + **notes, + **hs_dict, + **tm_dict, + **tc_dict, + } + headers = list(row_2.keys()) runs_and_robots[run_id] = row_2 else: - os.remove(file_path) - print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") - return runs_and_robots + continue + # os.remove(file_path) + # print(f"Run ID: {run_id} has a run time of 0 minutes. Run removed.") + return runs_and_robots, headers if __name__ == "__main__": @@ -122,7 +140,7 @@ def create_data_dictionary( metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", ) parser.add_argument( "google_sheet_name", @@ -131,11 +149,14 @@ def create_data_dictionary( nargs=1, help="Google sheet name.", ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) args = parser.parse_args() folder_name = args.folder_name[0] storage_directory = args.storage_directory[0] google_sheet_name = args.google_sheet_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -143,7 +164,7 @@ def create_data_dictionary( sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: @@ -160,23 +181,19 @@ def create_data_dictionary( except gspread.exceptions.APIError: print("ERROR: Check google sheet name. Check credentials file.") sys.exit() + try: + google_sheet_lpc = google_sheets_tool.google_sheet( + credentials_path, "ABR-LPC", 0 + ) + print("Connected to google sheet ABR-LPC") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() run_ids_on_gs = google_sheet.get_column(2) run_ids_on_gs = set(run_ids_on_gs) - # Read Google Drive .json files - google_drive_files = google_drive.list_folder() - google_drive_files_json = [ - file for file in google_drive_files if file.endswith(".json") - ] - # read local directory - list_of_files = os.listdir(storage_directory) - local_files_json = set( - file for file in os.listdir(storage_directory) if file.endswith(".json") - ) - missing_files = local_files_json - set(google_drive_files_json) - print(f"Missing files: {len(missing_files)}") # Uploads files that are not in google drive directory - google_drive.upload_missing_files(storage_directory, missing_files) + google_drive.upload_missing_files(storage_directory) # Run ids in google_drive_folder run_ids_on_gd = read_robot_logs.get_run_ids_from_google_drive(google_drive) @@ -184,29 +201,9 @@ def create_data_dictionary( run_ids_on_gd, run_ids_on_gs ) # Add missing runs to google sheet - runs_and_robots = create_data_dictionary(missing_runs_from_gs, storage_directory) - headers = [ - "Robot", - "Run_ID", - "Protocol_Name", - "Software Version", - "Date", - "Start_Time", - "End_Time", - "Run_Time (min)", - "Errors", - "Error_Code", - "Error_Type", - "Error_Instrument", - "Error_Level", - "Left Mount", - "Right Mount", - "Extension", - "heaterShakerModuleV1", - "temperatureModuleV2", - "magneticBlockV1", - "thermocyclerModuleV2", - ] + runs_and_robots, headers = create_data_dictionary( + missing_runs_from_gs, storage_directory, "" + ) read_robot_logs.write_to_local_and_google_sheet( runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers ) diff --git a/abr-testing/abr_testing/data_collection/abr_lpc.py b/abr-testing/abr_testing/data_collection/abr_lpc.py new file mode 100644 index 000000000000..dd880d09c376 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_lpc.py @@ -0,0 +1 @@ +"""Get Unique LPC Values from Run logs.""" diff --git a/abr-testing/abr_testing/data_collection/abr_robot_error.py b/abr-testing/abr_testing/data_collection/abr_robot_error.py new file mode 100644 index 000000000000..231b8077eedd --- /dev/null +++ b/abr-testing/abr_testing/data_collection/abr_robot_error.py @@ -0,0 +1,216 @@ +"""Create ticket for robot with error.""" +from typing import List, Tuple +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs +import requests +import argparse +from abr_testing.automation import jira_tool, google_sheets_tool, google_drive_tool +import shutil +import os +import subprocess +import json +import sys +import gspread # type: ignore[import] + + +def get_error_runs_from_robot(ip: str) -> List[str]: + """Get runs that have errors from robot.""" + error_run_ids = [] + response = requests.get( + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} + ) + run_data = response.json() + run_list = run_data["data"] + for run in run_list: + run_id = run["id"] + num_of_errors = len(run["errors"]) + if not run["current"] and num_of_errors > 0: + error_run_ids.append(run_id) + return error_run_ids + + +def get_error_info_from_robot( + ip: str, one_run: str, storage_directory: str +) -> Tuple[str, str, str, List[str], str, str]: + """Get error information from robot to fill out ticket.""" + description = dict() + # get run information + results = get_run_logs.get_run_data(one_run, ip) + # save run information to local directory as .json file + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, results, storage_directory + ) + # Error Printout + ( + num_of_errors, + error_type, + error_code, + error_instrument, + error_level, + ) = read_robot_logs.get_error_info(results) + # JIRA Ticket Fields + failure_level = "Level " + str(error_level) + " Failure" + components = [failure_level, "Flex-RABR"] + affects_version = results["API_Version"] + parent = results.get("robot_name", "") + print(parent) + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type + # Description of error + description["protocol_name"] = results["protocol"]["metadata"].get( + "protocolName", "" + ) + description["error"] = " ".join([error_code, error_type, error_instrument]) + description["protocol_step"] = list(results["commands"])[-1] + description["right_mount"] = results.get("right", "No attachment") + description["left_mount"] = results.get("left", "No attachment") + description["gripper"] = results.get("extension", "No attachment") + all_modules = abr_google_drive.get_modules(results) + whole_description = {**description, **all_modules} + whole_description_str = ( + "{" + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) + + "}" + ) + + return ( + summary, + parent, + affects_version, + components, + whole_description_str, + saved_file_path, + ) + + +if __name__ == "__main__": + """Create ticket for specified robot.""" + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "jira_api_token", + metavar="JIRA_API_TOKEN", + type=str, + nargs=1, + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", + ) + parser.add_argument( + "email", + metavar="EMAIL", + type=str, + nargs=1, + help="Email connected to JIRA account.", + ) + # TODO: write function to get reporter_id from email. + parser.add_argument( + "reporter_id", + metavar="REPORTER_ID", + type=str, + nargs=1, + help="JIRA Reporter ID.", + ) + # TODO: improve help comment on jira board id. + parser.add_argument( + "board_id", + metavar="BOARD_ID", + type=str, + nargs=1, + help="JIRA Board ID. RABR is 217", + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + ip = str(input("Enter Robot IP: ")) + url = "https://opentrons.atlassian.net" + api_token = args.jira_api_token[0] + email = args.email[0] + board_id = args.board_id[0] + reporter_id = args.reporter_id[0] + ticket = jira_tool.JiraTicket(url, api_token, email) + try: + error_runs = get_error_runs_from_robot(ip) + except requests.exceptions.InvalidURL: + print("Invalid IP address.") + sys.exit() + one_run = error_runs[-1] # Most recent run with error. + ( + summary, + robot, + affects_version, + components, + whole_description_str, + run_log_file_path, + ) = get_error_info_from_robot(ip, one_run, storage_directory) + # Get Calibration Data + saved_file_path_calibration, calibration = read_robot_logs.get_calibration_offsets( + ip, storage_directory + ) + file_paths = read_robot_logs.get_logs(storage_directory, ip) + print(f"Making ticket for run: {one_run} on robot {robot}.") + # TODO: make argument or see if I can get rid of with using board_id. + project_key = "RABR" + parent_key = project_key + "-" + robot[-1] + # TODO: read board to see if ticket for run id already exists. + # CREATE TICKET + issue_key = ticket.create_ticket( + summary, + whole_description_str, + project_key, + reporter_id, + "Bug", + "Medium", + components, + affects_version, + parent_key, + ) + # OPEN TICKET + issue_url = ticket.open_issue(issue_key) + # MOVE FILES TO ERROR FOLDER. + error_files = [saved_file_path_calibration, run_log_file_path] + file_paths + error_folder_path = os.path.join(storage_directory, issue_key) + os.makedirs(error_folder_path, exist_ok=True) + for source_file in error_files: + destination_file = os.path.join( + error_folder_path, os.path.basename(source_file) + ) + shutil.move(source_file, destination_file) + # OPEN FOLDER DIRECTORY + subprocess.Popen(["explorer", error_folder_path]) + # CONNECT TO GOOGLE DRIVE + credentials_path = os.path.join(storage_directory, "credentials.json") + google_sheet_name = "ABR-run-data" + try: + google_drive = google_drive_tool.google_drive( + credentials_path, + "1Cvej0eadFOTZr9ILRXJ0Wg65ymOtxL4m", + "rhyann.clarke@opentrons.ocm", + ) + print("Connected to google drive.") + except json.decoder.JSONDecodeError: + print( + "Credential file is damaged. Get from https://console.cloud.google.com/apis/credentials" + ) + sys.exit() + # CONNECT TO GOOGLE SHEET + try: + google_sheet = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 0 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + # WRITE ERRORED RUN TO GOOGLE SHEET + error_run_log = os.path.join(error_folder_path, os.path.basename(run_log_file_path)) + google_drive.upload_file(error_run_log) + run_id = os.path.basename(error_run_log).split("_")[1].split(".")[0] + runs_and_robots, headers = abr_google_drive.create_data_dictionary( + run_id, error_folder_path, issue_url + ) + read_robot_logs.write_to_local_and_google_sheet( + runs_and_robots, storage_directory, google_sheet_name, google_sheet, headers + ) + print("Wrote run to ABR-run-data") diff --git a/abr-testing/abr_testing/data_collection/error_levels.csv b/abr-testing/abr_testing/data_collection/error_levels.csv index c03cab56367c..c2f54c9f09ea 100644 --- a/abr-testing/abr_testing/data_collection/error_levels.csv +++ b/abr-testing/abr_testing/data_collection/error_levels.csv @@ -11,7 +11,7 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2000,Robotics Control Error,A Robot Action Failed,3, 2,2001,Motion Failed,A Robot Action Failed,4, 2,2002,Homing Failed,A Robot Action Failed,4, -2,2003,Stall or Collision Detected,A Robot Action Failed,3-4, +2,2003,Stall or Collision Detected,A Robot Action Failed,3, 2,2004,Motion Planning Failed,A Robot Action Failed,3, 2,2005,Position Estimation Invalid,A Robot Action Failed,3, 2,2006,Move Condition Not Met,A Robot Action Failed,3, @@ -20,29 +20,29 @@ Prefix,Error Code,Description,Categories,Level of Failure, 2,2009,Early Capactivive Sense Trigger,A Robot Action Failed,4, 2,2010,Innacrruate Non Contact Sweep,A Robot Action Failed,3, 2,2011,Misaligned Gantry,A Robot Action Failed,3, -2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4, +2,2012,Unmatched Tip Presence States,A Robot Action Failed, 4, 2,2013,Position Unknown,A Robot Action Failed,4, -2,2014,Execution Cancelled,A Robot Action Failed,3-4, -2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3-4, +2,2014,Execution Cancelled,A Robot Action Failed, 4, +2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3, 3,3000,Robotics Interaction Error,A Robot Interaction Failed,3, -3,3001,Labware Dropped,A Robot Interaction Failed,3-4, -3,3002,Labware Not Picked Up,A Robot Interaction Failed,3-4, +3,3001,Labware Dropped,A Robot Interaction Failed, 4, +3,3002,Labware Not Picked Up,A Robot Interaction Failed,4, 3,3003,Tip Pickup Failed,A Robot Interaction Failed,4, 3,3004,Tip Drop Failed,A Robot Interaction Failed,4, 3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4, -3,3006,Pipette Overpressure,A Robot Interaction Failed,3-4, -3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error, +3,3006,Pipette Overpressure,A Robot Interaction Failed,3, +3,3008,E-Stop Activated,A Robot Interaction Failed,5, Not an error, 3,3009,E-Stop Not Present,A Robot Interaction Failed,5, 3,3010,Pipette Not Present,A Robot Interaction Failed,5, 3,3011,Gripper Not Present,A Robot Interaction Failed,5, 3,3012,Unexpected Tip Attach,A Robot Interaction Failed,4, -3,3013,Firmware Update Required,A Robot Interaction Failed,Not an error, +3,3013,Firmware Update Required,A Robot Interaction Failed,5, Not an error, 3,3014,Invalid ID Actuator,A Robot Interaction Failed,3, 3,3015,Module Not Pesent,A Robot Interaction Failed,5,Not an error 3,3016,Invalid Instrument Data,A Robot Interaction Failed,3, 3,3017,Invalid Liquid Class Name,A Robot Interaction Failed,5,Not an error 3,3018,Tip Detector Not Found,A Robot Interaction Failed,3, -4,4000,General Error,A Software Error Occured,2-4,How severe does a general error get +4,4000,General Error,A Software Error Occured,4,How severe does a general error get 4,4001,Robot In Use,A Software Error Occured,5,Not an error 4,4002,API Removed,A Software Error Occured,5,used an old app on a new robot 4,4003,Not Supported On Robot Type,A Software Error Occured,5,Not an error diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py index f80a4fb9f77b..4034f076dc92 100644 --- a/abr-testing/abr_testing/data_collection/get_run_logs.py +++ b/abr-testing/abr_testing/data_collection/get_run_logs.py @@ -6,7 +6,7 @@ import requests import sys from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_drive_tool +from abr_testing.automation import google_drive_tool def get_run_ids_from_robot(ip: str) -> Set[str]: @@ -80,9 +80,9 @@ def save_runs(runs_to_save: Set[str], ip: str, storage_directory: str) -> Set[st saved_file_paths = set() for a_run in runs_to_save: data = get_run_data(a_run, ip) - data_file_name = ip + "_" + data["run_id"] + ".json" - saved_file_path = os.path.join(storage_directory, data_file_name) - json.dump(data, open(saved_file_path, mode="w")) + saved_file_path = read_robot_logs.save_run_log_to_json( + ip, data, storage_directory + ) saved_file_paths.add(saved_file_path) print(f"Saved {len(runs_to_save)} run(s) from robot with IP address {ip}.") return saved_file_paths @@ -107,8 +107,8 @@ def get_all_run_logs(storage_directory: str) -> None: try: runs = get_run_ids_from_robot(ip) runs_to_save = read_robot_logs.get_unseen_run_ids(runs, runs_from_storage) - saved_file_paths = save_runs(runs_to_save, ip, storage_directory) - google_drive.upload_missing_files(storage_directory, saved_file_paths) + save_runs(runs_to_save, ip, storage_directory) + google_drive.upload_missing_files(storage_directory) except Exception: print(f"ERROR: Failed to read IP address: {ip}.") @@ -128,12 +128,15 @@ def get_all_run_logs(storage_directory: str) -> None: metavar="FOLDER_NAME", type=str, nargs=1, - help="Google Drive folder name.", + help="Google Drive folder name. Open desired folder and copy string after drive/folders/.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." ) args = parser.parse_args() storage_directory = args.storage_directory[0] folder_name = args.folder_name[0] - parent_folder = False + email = args.email[0] try: credentials_path = os.path.join(storage_directory, "credentials.json") except FileNotFoundError: @@ -141,7 +144,7 @@ def get_all_run_logs(storage_directory: str) -> None: sys.exit() try: google_drive = google_drive_tool.google_drive( - credentials_path, folder_name, parent_folder + credentials_path, folder_name, email ) print("Connected to google drive.") except json.decoder.JSONDecodeError: diff --git a/abr-testing/abr_testing/data_collection/module_ramp_rates.py b/abr-testing/abr_testing/data_collection/module_ramp_rates.py new file mode 100644 index 000000000000..dc402071bb72 --- /dev/null +++ b/abr-testing/abr_testing/data_collection/module_ramp_rates.py @@ -0,0 +1,154 @@ +"""Get ramp rates of modules.""" +from abr_testing.automation import google_sheets_tool +from abr_testing.data_collection import read_robot_logs +import gspread # type: ignore[import] +import argparse +import os +import sys +import json +from datetime import datetime +from typing import Dict, Any +import requests + + +def ramp_rate(file_results: Dict[str, Any]) -> Dict[int, float]: + """Get ramp rates.""" + i = 0 + commands = file_results["commands"] + for command in commands: + commandType = command["commandType"] + if ( + commandType == "thermocycler/setTargetBlockTemperature" + or commandType == "temperatureModule/setTargetTemperature" + or commandType == "heaterShaker/setTargetTemperature" + ): + temp = command["params"].get("celsius", 0.0) + if ( + commandType == "thermocycler/waitForBlockTemperature" + or commandType == "temperatureModule/waitForTemperature" + or commandType == "heaterShaker/waitForTemperature" + ): + start_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + end_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + duration = (end_time - start_time).total_seconds() + i += 1 + temps_and_durations[duration] = temp + ramp_rates = {} + times = list(temps_and_durations.keys()) + for i in range(len(times) - 1): + time1 = times[i] + time2 = times[i + 1] + temp1 = temps_and_durations[time1] + temp2 = temps_and_durations[time2] + ramp_rate = (temp2 - temp1) / (time2) + ramp_rates[i] = ramp_rate + return ramp_rates + + +if __name__ == "__main__": + # SCRIPT ARGUMENTS + parser = argparse.ArgumentParser(description="Read run logs on google drive.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "google_sheet_name", + metavar="GOOGLE_SHEET_NAME", + type=str, + nargs=1, + help="Google sheet name.", + ) + parser.add_argument( + "email", metavar="EMAIL", type=str, nargs=1, help="opentrons gmail." + ) + args = parser.parse_args() + storage_directory = args.storage_directory[0] + google_sheet_name = args.google_sheet_name[0] + # FIND CREDENTIALS FILE + try: + credentials_path = os.path.join(storage_directory, "credentials.json") + except FileNotFoundError: + print(f"Add credentials.json file to: {storage_directory}.") + sys.exit() + # CONNECT TO GOOGLE SHEET + try: + google_sheet = google_sheets_tool.google_sheet( + credentials_path, google_sheet_name, 1 + ) + print(f"Connected to google sheet: {google_sheet_name}") + except gspread.exceptions.APIError: + print("ERROR: Check google sheet name. Check credentials file.") + sys.exit() + run_ids_on_sheet = google_sheet.get_column(2) + runs_and_robots = {} + for filename in os.listdir(storage_directory): + file_path = os.path.join(storage_directory, filename) + if file_path.endswith(".json"): + with open(file_path) as file: + file_results = json.load(file) + else: + continue + # CHECK if file is ramp rate run + run_id = file_results.get("run_id", None) + temps_and_durations: Dict[float, float] = dict() + if run_id is not None and run_id not in run_ids_on_sheet: + + ramp_rates = ramp_rate(file_results) + protocol_name = file_results["protocol"]["metadata"].get("protocolName", "") + if "Ramp Rate" in protocol_name: + ip = filename.split("_")[0] + if len(ramp_rates) > 1: + cooling_ramp_rate = abs(min(ramp_rates.values())) + heating_ramp_rate = abs(max(ramp_rates.values())) + start_time = datetime.strptime( + file_results.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + start_date = str(start_time.date()) + module_serial_number = file_results["modules"][0].get( + "serialNumber", "NaN" + ) + try: + response = requests.get( + f"http://{ip}:31950/modules", + headers={"opentrons-version": "3"}, + ) + modules = response.json() + for module in modules["data"]: + if module["serialNumber"] == module_serial_number: + firmwareVersion = module["firmwareVersion"] + else: + firmwareVersion = "NaN" + except requests.exceptions.ConnectionError: + firmwareVersion = "NaN" + row = { + "Robot": file_results.get("robot_name", ""), + "Run_ID": run_id, + "Protocol_Name": file_results["protocol"]["metadata"].get( + "protocolName", "" + ), + "Software Version": file_results.get("API_Version", ""), + "Firmware Version": firmwareVersion, + "Date": start_date, + "Serial Number": module_serial_number, + "Approx. Average Heating Ramp Rate (C/s)": heating_ramp_rate, + "Approx. Average Cooling Ramp Rate (C/s)": cooling_ramp_rate, + } + headers = list(row.keys()) + runs_and_robots[run_id] = row + read_robot_logs.write_to_local_and_google_sheet( + runs_and_robots, + storage_directory, + google_sheet_name, + google_sheet, + headers, + ) + else: + continue 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 d30842b33fd7..48ef1d20163f 100644 --- a/abr-testing/abr_testing/data_collection/read_robot_logs.py +++ b/abr-testing/abr_testing/data_collection/read_robot_logs.py @@ -1,15 +1,250 @@ """ABR Read Robot Logs. -This library is downloading logs from robots, extracting wanted information, +This library has functions to download logs from robots, extracting wanted information, and uploading to a google sheet using credentials and google_sheets_tools module saved in a local directory. """ import csv +from datetime import datetime import os from abr_testing.data_collection.error_levels import ERROR_LEVELS_PATH from typing import List, Dict, Any, Tuple, Set import time as t import json +import requests + + +def lpc_data(file_results: Dict[str, Any], protocol_info: Dict) -> List[Dict[str, Any]]: + """Get labware offsets from one run log.""" + offsets = file_results.get("labwareOffsets", "") + all_offsets: List[Dict[str, Any]] = [] + if len(offsets) > 0: + for offset in offsets: + labware_type = offset.get("definitionUri", "") + slot = offset["location"].get("slotName", "") + module_location = offset["location"].get("moduleModel", "") + adapter = offset["location"].get("definitionUri", "") + x_offset = offset["vector"].get("x", 0.0) + y_offset = offset["vector"].get("y", 0.0) + z_offset = offset["vector"].get("z", 0.0) + created_at = offset.get("createdAt", "") + row = { + "createdAt": created_at, + "Labware Type": labware_type, + "Slot": slot, + "Module": module_location, + "Adapter": adapter, + "X": x_offset, + "Y": y_offset, + "Z": z_offset, + } + row2 = {**protocol_info, **row} + all_offsets.append(row2) + return all_offsets + + +def command_time(command: Dict[str, str]) -> Tuple[float, float]: + """Calculate total create and complete time per command.""" + try: + create_time = datetime.strptime( + command.get("createdAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + start_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + complete_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + create_to_start = (start_time - create_time).total_seconds() + start_to_complete = (complete_time - start_time).total_seconds() + except ValueError: + create_to_start = 0 + start_to_complete = 0 + return create_to_start, start_to_complete + + +def hs_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Gets total latch engagements, homes, rotations and total on time (sec) for heater shaker.""" + # TODO: modify for cases that have more than 1 heater shaker. + commandData = file_results.get("commands", "") + hs_latch_count: float = 0.0 + hs_temp: float = 0.0 + hs_home_count: float = 0.0 + hs_speed: float = 0.0 + hs_rotations: Dict[str, float] = dict() + hs_temps: Dict[str, float] = dict() + temp_time = None + shake_time = None + for command in commandData: + commandType = command["commandType"] + # Heatershaker + # Latch count + if ( + commandType == "heaterShaker/closeLabwareLatch" + or commandType == "heaterShaker/openLabwareLatch" + ): + hs_latch_count += 1 + # Home count + elif commandType == "heaterShaker/deactivateShaker": + hs_home_count += 1 + deactivate_time = datetime.strptime( + command.get("startedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + hs_temps[hs_temp] = hs_temps.get(hs_temp, 0.0) + temp_duration + if shake_time is not None and deactivate_time > shake_time: + shake_duration = (deactivate_time - shake_time).total_seconds() + hs_rotations[hs_speed] = hs_rotations.get(hs_speed, 0.0) + ( + (hs_speed * shake_duration) / 60 + ) + # of Rotations + elif commandType == "heaterShaker/setAndWaitForShakeSpeed": + hs_speed = command["params"]["rpm"] + shake_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + # On Time + elif commandType == "heaterShaker/setTargetTemperature": + # if heater shaker temp is not deactivated. + hs_temp = command["params"]["celsius"] + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + hs_latch_sets = hs_latch_count / 2 # one set of open/close + hs_total_rotations = sum(hs_rotations.values()) + hs_total_temp_time = sum(hs_temps.values()) + hs_dict = { + "Heatershaker # of Latch Open/Close": hs_latch_sets, + "Heatershaker # of Homes": hs_home_count, + "Heatershaker # of Rotations": hs_total_rotations, + "Heatershaker Temp On Time (sec)": hs_total_temp_time, + } + return hs_dict + + +def temperature_module_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Get # of temp changes and total temp on time for temperature module from run log.""" + # TODO: modify for cases that have more than 1 temperature module. + tm_temp_change = 0 + tm_temps: Dict[str, float] = dict() + temp_time = None + deactivate_time = None + commandData = file_results.get("commands", "") + for command in commandData: + commandType = command["commandType"] + if commandType == "temperatureModule/setTargetTemperature": + tm_temp = command["params"]["celsius"] + tm_temp_change += 1 + if commandType == "temperatureModule/waitForTemperature": + temp_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "temperatureModule/deactivate": + deactivate_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if temp_time is not None and deactivate_time > temp_time: + temp_duration = (deactivate_time - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + if temp_time is not None and deactivate_time is None: + # If temperature module is not deactivated, protocol completedAt time stamp used. + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - temp_time).total_seconds() + tm_temps[tm_temp] = tm_temps.get(tm_temp, 0.0) + temp_duration + tm_total_temp_time = sum(tm_temps.values()) + tm_dict = { + "Temp Module # of Temp Changes": tm_temp_change, + "Temp Module Temp On Time (sec)": tm_total_temp_time, + } + return tm_dict + + +def thermocycler_commands(file_results: Dict[str, Any]) -> Dict[str, float]: + """Counts # of lid engagements, temp changes, and temp sustaining mins.""" + # TODO: modify for cases that have more than 1 thermocycler. + commandData = file_results.get("commands", "") + lid_engagements: float = 0.0 + block_temp_changes: float = 0.0 + lid_temp_changes: float = 0.0 + lid_temps: Dict[str, float] = dict() + block_temps: Dict[str, float] = dict() + lid_on_time = None + lid_off_time = None + block_on_time = None + block_off_time = None + for command in commandData: + commandType = command["commandType"] + if ( + commandType == "thermocycler/openLid" + or commandType == "thermocycler/closeLid" + ): + lid_engagements += 1 + if commandType == "thermocycler/setTargetBlockTemperature": + block_temp = command["params"]["celsius"] + block_temp_changes += 1 + block_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/setTargetLidTemperature": + lid_temp_changes += 1 + lid_temp = command["params"]["celsius"] + lid_on_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if commandType == "thermocycler/deactivateLid": + lid_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if lid_on_time is not None and lid_off_time > lid_on_time: + lid_duration = (lid_off_time - lid_on_time).total_seconds() + lid_temps[lid_temp] = lid_temps.get(lid_temp, 0.0) + lid_duration + if commandType == "thermocycler/deactivateBlock": + block_off_time = datetime.strptime( + command.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + if block_on_time is not None and block_off_time > block_on_time: + block_duration = (block_off_time - block_on_time).total_seconds() + block_temps[block_temp] = ( + block_temps.get(block_temp, 0.0) + block_duration + ) + if commandType == "thermocycler/runProfile": + profile = command["params"]["profile"] + total_changes = len(profile) + block_temp_changes += total_changes + for cycle in profile: + block_temp = cycle["celsius"] + block_time = cycle["holdSeconds"] + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + block_time + if block_on_time is not None and block_off_time is None: + # If thermocycler block not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - block_on_time).total_seconds() + block_temps[block_temp] = block_temps.get(block_temp, 0.0) + temp_duration + if lid_on_time is not None and lid_off_time is None: + # If thermocycler lid not deactivated protocol completedAt time stamp used + protocol_end = datetime.strptime( + file_results.get("completedAt", ""), "%Y-%m-%dT%H:%M:%S.%f%z" + ) + temp_duration = (protocol_end - lid_on_time).total_seconds() + lid_temps[lid_temp] = block_temps.get(lid_temp, 0.0) + temp_duration + + block_total_time = sum(block_temps.values()) + lid_total_time = sum(lid_temps.values()) + lid_sets = lid_engagements / 2 + tc_dict = { + "Thermocycler # of Lid Open/Close": lid_sets, + "Thermocycler Block # of Temp Changes": block_temp_changes, + "Thermocycler Block Temp On Time (sec)": block_total_time, + "Thermocycler Lid # of Temp Changes": lid_temp_changes, + "Thermocycler Lid Temp On Time (sec)": lid_total_time, + } + + return tc_dict def create_abr_data_sheet( @@ -17,7 +252,6 @@ def create_abr_data_sheet( ) -> str: """Creates csv file to log ABR data.""" file_name_csv = file_name + ".csv" - print(file_name_csv) sheet_location = os.path.join(storage_directory, file_name_csv) if os.path.exists(sheet_location): print(f"File {sheet_location} located. Not overwriting.") @@ -26,7 +260,7 @@ def create_abr_data_sheet( writer = csv.DictWriter(csvfile, fieldnames=headers) writer.writeheader() print(f"Created file. Located: {sheet_location}.") - return file_name_csv + return sheet_location def get_error_info(file_results: Dict[str, Any]) -> Tuple[int, str, str, str, str]: @@ -110,7 +344,7 @@ def read_abr_data_sheet( runs_in_sheet.add(run_id) print(f"There are {str(len(runs_in_sheet))} runs documented in the ABR sheet.") # Read Google Sheet - google_sheet.check_token() + google_sheet.token_check() google_sheet.write_header(headers) google_sheet.update_row_index() return runs_in_sheet @@ -138,6 +372,16 @@ def get_unseen_run_ids(runs: Set[str], runs_from_storage: Set[str]) -> Set[str]: return runs_to_save +def save_run_log_to_json( + ip: str, results: Dict[str, Any], storage_directory: str +) -> str: + """Save run log to local json file.""" + data_file_name = ip + "_" + results["run_id"] + ".json" + saved_file_path = os.path.join(storage_directory, data_file_name) + json.dump(results, open(saved_file_path, mode="w")) + return saved_file_path + + def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: """Get run ids in google drive folder.""" # Run ids in google_drive_folder @@ -148,3 +392,91 @@ def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]: file_id = file.split(".json")[0].split("_")[1] run_ids_on_gd.add(file_id) return run_ids_on_gd + + +def write_to_sheets( + sheet_location: str, google_sheet: Any, row_list: List[Any], headers: List[str] +) -> None: + """Write list to google sheet and csv.""" + with open(sheet_location, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow(row_list) + # Read Google Sheet + google_sheet.token_check() + google_sheet.write_header(headers) + google_sheet.update_row_index() + google_sheet.write_to_row(row_list) + t.sleep(5) # Sleep added to avoid API error. + + +def get_calibration_offsets( + ip: str, storage_directory: str +) -> Tuple[str, Dict[str, Any]]: + """Connect to robot via ip and get calibration data.""" + calibration = dict() + # Robot Information [Name, Software Version] + response = requests.get( + f"http://{ip}:31950/health", headers={"opentrons-version": "3"} + ) + health_data = response.json() + robot_name = health_data.get("name", "") + api_version = health_data.get("api_version", "") + pull_date_timestamp = datetime.now() + date = pull_date_timestamp.date().isoformat() + file_date = str(pull_date_timestamp).replace(":", "").split(".")[0] + calibration["Robot"] = robot_name + calibration["Software Version"] = api_version + calibration["Pull Date"] = date + calibration["Pull Timestamp"] = pull_date_timestamp.isoformat() + calibration["run_id"] = "calibration" + "_" + file_date + # Calibration [Instruments, modules, deck] + response = requests.get( + f"http://{ip}:31950/instruments", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + instruments: Dict[str, Any] = response.json() + calibration["Instruments"] = instruments.get("data", "") + response = requests.get( + f"http://{ip}:31950/modules", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + modules: Dict[str, Any] = response.json() + calibration["Modules"] = modules.get("data", "") + response = requests.get( + f"http://{ip}:31950/calibration/status", + headers={"opentrons-version": "3"}, + params={"cursor": 0, "pageLength": 0}, + ) + deck: Dict[str, Any] = response.json() + calibration["Deck"] = deck.get("deckCalibration", "") + save_name = ip + "_calibration.json" + saved_file_path = os.path.join(storage_directory, save_name) + json.dump(calibration, open(saved_file_path, mode="w")) + return saved_file_path, calibration + + +def get_logs(storage_directory: str, ip: str) -> List[str]: + """Get Robot logs.""" + log_types = ["api.log", "server.log", "serial.log", "touchscreen.log"] + all_paths = [] + for log_type in log_types: + try: + response = requests.get( + f"http://{ip}:31950/logs/{log_type}", + headers={"log_identifier": log_type}, + params={"records": 5000}, + ) + response.raise_for_status() + log_data = response.text + log_name = ip + "_" + log_type.split(".")[0] + ".json" + file_path = os.path.join(storage_directory, log_name) + with open(file_path, mode="w", encoding="utf-8") as file: + file.write(response.text) + json.dump(log_data, open(file_path, mode="w")) + except RuntimeError: + print(f"Request exception. Did not save {log_type}") + continue + all_paths.append(file_path) + return all_paths diff --git a/abr-testing/abr_testing/tools/abr_asair_sensor.py b/abr-testing/abr_testing/tools/abr_asair_sensor.py index 4183b812930b..eef69329436f 100644 --- a/abr-testing/abr_testing/tools/abr_asair_sensor.py +++ b/abr-testing/abr_testing/tools/abr_asair_sensor.py @@ -6,7 +6,7 @@ import time as t from typing import List import argparse -from abr_testing.google_automation import google_sheets_tool +from abr_testing.automation import google_sheets_tool class _ABRAsairSensor: diff --git a/abr-testing/abr_testing/tools/abr_scale.py b/abr-testing/abr_testing/tools/abr_scale.py index 5d253d25c704..75c887d4ecc5 100644 --- a/abr-testing/abr_testing/tools/abr_scale.py +++ b/abr-testing/abr_testing/tools/abr_scale.py @@ -3,26 +3,9 @@ import datetime from hardware_testing.drivers import find_port, list_ports_and_select # type: ignore[import] from hardware_testing.drivers.radwag import RadwagScale # type: ignore[import] -from typing import Any, List import argparse -import csv from abr_testing.data_collection import read_robot_logs -from abr_testing.google_automation import google_sheets_tool - - -def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None: - """Write list to google sheet and csv.""" - sheet_location = os.path.join(storage_directory, file_name_csv) - with open(sheet_location, "a", newline="") as f: - writer = csv.writer(f) - writer.writerow(row_list) - print(f"Written {row_list} point to {file_name_csv}") - # Read Google Sheet - google_sheet.token_check() - google_sheet.write_header(headers) - google_sheet.update_row_index() - google_sheet.write_to_row(row_list) - print(f"Written {row_list} to google sheet.") +from abr_testing.automation import google_sheets_tool if __name__ == "__main__": @@ -76,7 +59,7 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No is_stable = False # Set up csv sheet headers = ["Robot", "Date", "Timestamp", "Labware", "Mass (g)", "Measurement Step"] - all_data_csv = read_robot_logs.create_abr_data_sheet( + sheet_location = read_robot_logs.create_abr_data_sheet( storage_directory, file_name, headers ) # Set up google sheet @@ -90,8 +73,12 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No print("No google sheets credentials. Add credentials to storage notebook.") # Scale Loop + grams, is_stable = scale.read_mass() + grams, is_stable = scale.read_mass() + is_stable = False break_all = False while is_stable is False: + grams, is_stable = scale.read_mass() grams, is_stable = scale.read_mass() print(f"Scale reading: grams={grams}, is_stable={is_stable}") time_now = datetime.datetime.now() @@ -100,14 +87,19 @@ def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> No row_list = list(row) while is_stable is True: print("is stable") - write_to_sheets(file_name_csv, google_sheet, row_list) + read_robot_logs.write_to_sheets( + sheet_location, google_sheet, row_list, headers + ) is_stable = False y_or_no = input("Do you want to weigh another sample? (Y/N): ") if y_or_no == "Y": # Uses same storage directory and file. + grams, is_stable = scale.read_mass() + is_stable = False robot = input("Robot: ") labware = input("Labware: ") protocol_step = input("Measurement Step (1,2,3): ") + grams, is_stable = scale.read_mass() elif y_or_no == "N": break_all = True if break_all: diff --git a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json index df8fcad1d987..74faa60fcb68 100644 --- a/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json +++ b/api-client/src/protocols/__fixtures__/simpleAnalysisFile.json @@ -3936,5 +3936,58 @@ "description": "", "displayColor": "#b925ff" } + ], + "runTimeParameters": [ + { + "type": "int", + "displayName": "number of samples", + "variableName": "num_samples", + "description": "How many samples do you want to run?", + "value": 96, + "min": 1, + "max": 96, + "default": 96 + }, + { + "type": "float", + "displayName": "samples volume", + "variableName": "vol_sample", + "description": "What sample volume are you using?", + "value": 10.0, + "min": 1, + "max": 20.0, + "default": 10.0 + }, + { + "displayName": "Additional mix for reagent 2?", + "variableName": "extra_mix", + "description": "When on, we do an extra mix for reagent 2.", + "type": "bool", + "default": false, + "value": false + }, + { + "displayName": "Number of PCR Cycles", + "variableName": "real_mode", + "description": "Cycle map", + "type": "int", + "unit": "cycles", + "default": 15, + "value": 15, + "choices": [ + { + "displayName": "1 & 10ng (15 cycles)", + "value": 15 + }, + { + "displayName": "100ng (15 cycles)", + "value": 15 + }, + { + "displayName": "1ug (10 cycles)", + "value": 10 + } + ] + } ] } diff --git a/api-client/src/protocols/createProtocol.ts b/api-client/src/protocols/createProtocol.ts index 64593d1a9537..2bcbefe6a7b6 100644 --- a/api-client/src/protocols/createProtocol.ts +++ b/api-client/src/protocols/createProtocol.ts @@ -2,15 +2,22 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { Protocol } from './types' +import type { RunTimeParameterCreateData } from '../runs' export function createProtocol( config: HostConfig, files: File[], - protocolKey?: string + protocolKey?: string, + runTimeParameterValues?: RunTimeParameterCreateData ): ResponsePromise { const formData = new FormData() files.forEach(file => formData.append('files', file, file.name)) if (protocolKey != null) formData.append('key', protocolKey) + if (runTimeParameterValues != null) + formData.append( + 'runTimeParameterValues', + JSON.stringify(runTimeParameterValues) + ) return request(POST, '/protocols', formData, config) } diff --git a/api-client/src/protocols/createProtocolAnalysis.ts b/api-client/src/protocols/createProtocolAnalysis.ts new file mode 100644 index 000000000000..81ab83c11af2 --- /dev/null +++ b/api-client/src/protocols/createProtocolAnalysis.ts @@ -0,0 +1,28 @@ +import { POST, request } from '../request' + +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RunTimeParameterCreateData } from '../runs' + +interface CreateProtocolAnalysisData { + runTimeParameterValues: RunTimeParameterCreateData + forceReAnalyze: boolean +} + +export function createProtocolAnalysis( + config: HostConfig, + protocolKey: string, + runTimeParameterValues?: RunTimeParameterCreateData, + forceReAnalyze?: boolean +): ResponsePromise { + const data = { + runTimeParameterValues: runTimeParameterValues ?? {}, + forceReAnalyze: forceReAnalyze ?? false, + } + const response = request< + ProtocolAnalysisSummary[], + { data: CreateProtocolAnalysisData } + >(POST, `/protocols/${protocolKey}/analyses`, { data }, config) + return response +} diff --git a/api-client/src/protocols/index.ts b/api-client/src/protocols/index.ts index 6febd0795cfe..f035fa000e14 100644 --- a/api-client/src/protocols/index.ts +++ b/api-client/src/protocols/index.ts @@ -3,6 +3,7 @@ export { getProtocolAnalyses } from './getProtocolAnalyses' export { getProtocolAnalysisAsDocument } from './getProtocolAnalysisAsDocument' export { deleteProtocol } from './deleteProtocol' export { createProtocol } from './createProtocol' +export { createProtocolAnalysis } from './createProtocolAnalysis' export { getProtocols } from './getProtocols' export { getProtocolIds } from './getProtocolIds' diff --git a/api-client/src/robot/getRobotSettings.ts b/api-client/src/robot/getRobotSettings.ts new file mode 100644 index 000000000000..ffe0014fcb02 --- /dev/null +++ b/api-client/src/robot/getRobotSettings.ts @@ -0,0 +1,11 @@ +import { GET, request } from '../request' + +import type { ResponsePromise } from '../request' +import type { HostConfig } from '../types' +import type { RobotSettingsResponse } from './types' + +export function getRobotSettings( + config: HostConfig +): ResponsePromise { + return request(GET, '/settings', null, config) +} diff --git a/api-client/src/robot/index.ts b/api-client/src/robot/index.ts index 96ef28165b03..588a2f7a80ee 100644 --- a/api-client/src/robot/index.ts +++ b/api-client/src/robot/index.ts @@ -3,11 +3,16 @@ export { getEstopStatus } from './getEstopStatus' export { acknowledgeEstopDisengage } from './acknowledgeEstopDisengage' export { getLights } from './getLights' export { setLights } from './setLights' +export { getRobotSettings } from './getRobotSettings' + export type { DoorStatus, EstopPhysicalStatus, EstopState, EstopStatus, Lights, + RobotSettings, + RobotSettingsField, + RobotSettingsResponse, SetLightsData, } from './types' diff --git a/api-client/src/robot/types.ts b/api-client/src/robot/types.ts index 00d887b9c4e4..088d78fa5c85 100644 --- a/api-client/src/robot/types.ts +++ b/api-client/src/robot/types.ts @@ -27,3 +27,18 @@ export interface Lights { export interface SetLightsData { on: boolean } + +export interface RobotSettingsField { + id: string + title: string + description: string + value: boolean | null + restart_required?: boolean +} + +export type RobotSettings = RobotSettingsField[] + +export interface RobotSettingsResponse { + settings: RobotSettings + links?: { restart?: string } +} diff --git a/api-client/src/runs/createRun.ts b/api-client/src/runs/createRun.ts index 5b2883917c68..7f0fb1ad72d3 100644 --- a/api-client/src/runs/createRun.ts +++ b/api-client/src/runs/createRun.ts @@ -5,13 +5,13 @@ import type { HostConfig } from '../types' import type { Run, LabwareOffsetCreateData, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from './types' export interface CreateRunData { protocolId?: string labwareOffsets?: LabwareOffsetCreateData[] - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData } export function createRun( diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 0be2a9973ed4..7e6ec2b0ee79 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -47,6 +47,7 @@ export interface LegacyGoodRunData { modules: LoadedModule[] protocolId?: string labwareOffsets?: LabwareOffset[] + runTimeParameterValues?: RunTimeParameterCreateData } export interface KnownGoodRunData extends LegacyGoodRunData { @@ -125,7 +126,7 @@ export interface LabwareOffsetCreateData { vector: VectorOffset } -export interface RuntimeParameterCreateData { +export interface RunTimeParameterCreateData { [key: string]: string | boolean | number } diff --git a/api-client/src/subsystems/types.ts b/api-client/src/subsystems/types.ts index 14f45324f62c..564d59b21b20 100644 --- a/api-client/src/subsystems/types.ts +++ b/api-client/src/subsystems/types.ts @@ -6,6 +6,7 @@ export type Subsystem = | 'pipette_right' | 'gripper' | 'rear_panel' + | 'hepa_uv' type UpdateStatus = 'queued' | 'updating' | 'done' export interface SubsystemUpdateProgressData { diff --git a/api/.flake8 b/api/.flake8 index d654020fa7f1..ee1a726e6112 100644 --- a/api/.flake8 +++ b/api/.flake8 @@ -33,7 +33,7 @@ per-file-ignores = src/opentrons/simulate.py:ANN,D src/opentrons/types.py:ANN,D src/opentrons/calibration_storage/*:ANN,D - src/opentrons/commands/*:D + src/opentrons/legacy_commands/*:D src/opentrons/config/*:ANN,D src/opentrons/drivers/*:ANN,D src/opentrons/hardware_control/*:ANN,D @@ -51,7 +51,7 @@ per-file-ignores = tests/opentrons/test_types.py:ANN,D tests/opentrons/conftest.py:ANN,D tests/opentrons/calibration_storage/*:ANN,D - tests/opentrons/commands/*:ANN,D + tests/opentrons/legacy_commands/*:ANN,D tests/opentrons/config/*:ANN,D tests/opentrons/data/*:ANN,D tests/opentrons/drivers/*:ANN,D diff --git a/api/Pipfile b/api/Pipfile index 710a5cb6f222..7be11b82934a 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -48,3 +48,4 @@ pytest-profiling = "~=1.7.0" # TODO(mc, 2022-03-31): upgrade sphinx, remove this subdep pin jinja2 = ">=2.3,<3.1" hypothesis = "==6.96.1" +performance-metrics = {file = "../performance-metrics", editable = true} diff --git a/api/Pipfile.lock b/api/Pipfile.lock index cc9f3163e514..94643ce22a75 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f0d4979ecb4f125cef848e0ce31e3a5e9cded69abaf773ad90d00016f6d2a65d" + "sha256": "a531665bfd7452ea19565ee95137118966532a8ab5475b7d5ee086ada333e627" }, "pipfile-spec": 6, "requires": {}, @@ -56,11 +56,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "jsonschema": { "hashes": [ @@ -73,65 +73,65 @@ }, "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "version": "==1.0.8" }, "numpy": { "hashes": [ @@ -162,6 +162,7 @@ }, "opentrons": { "editable": true, + "markers": "python_version >= '3.8'", "path": "." }, "opentrons-hardware": { @@ -173,15 +174,16 @@ }, "opentrons-shared-data": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../shared-data/python" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pydantic": { "hashes": [ @@ -276,32 +278,31 @@ "sha256:6ad50f4613289f3c4d276b6d2ac8901d776dcb929994cce93f55a69e858c595f", "sha256:7eea9b81b0ff908000a825db024313f622895bd578e8a17433e0474cd7d2da83" ], - "markers": "python_version >= '3.7'", "version": "==4.2.2" }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.5.1" }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "wrapt": { "hashes": [ @@ -413,6 +414,14 @@ "markers": "python_version >= '3.7'", "version": "==2.14.0" }, + "backports.tarfile": { + "hashes": [ + "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75", + "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4" + ], + "markers": "python_version < '3.12'", + "version": "==1.0.0" + }, "black": { "hashes": [ "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", @@ -445,11 +454,69 @@ }, "certifi": { "hashes": [ - "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2023.11.17" + "version": "==2024.2.2" + }, + "cffi": { + "hashes": [ + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" }, "charset-normalizer": { "hashes": [ @@ -565,53 +632,53 @@ }, "contourpy": { "hashes": [ - "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", - "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956", - "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5", - "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", - "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", - "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", - "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", - "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", - "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", - "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4", - "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", - "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", - "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e", - "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", - "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", - "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", - "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", - "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", - "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", - "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", - "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", - "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", - "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc", - "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", - "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", - "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808", - "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", - "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", - "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843", - "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", - "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", - "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9", - "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", - "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", - "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", - "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", - "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8", - "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776", - "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", - "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108", - "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e", - "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8", - "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", - "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a" + "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2", + "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9", + "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9", + "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4", + "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce", + "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7", + "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f", + "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922", + "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4", + "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e", + "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b", + "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619", + "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205", + "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480", + "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965", + "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c", + "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd", + "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5", + "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f", + "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc", + "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec", + "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd", + "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b", + "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9", + "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe", + "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce", + "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609", + "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8", + "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0", + "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f", + "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8", + "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b", + "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364", + "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040", + "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f", + "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083", + "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df", + "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba", + "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445", + "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da", + "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3", + "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72", + "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02", + "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985" ], "markers": "python_version >= '3.9'", - "version": "==1.2.0" + "version": "==1.2.1" }, "coverage": { "extras": [ @@ -675,6 +742,44 @@ "markers": "python_version >= '3.8'", "version": "==7.4.1" }, + "cryptography": { + "hashes": [ + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + ], + "markers": "python_version >= '3.7'", + "version": "==42.0.5" + }, "cycler": { "hashes": [ "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", @@ -710,11 +815,11 @@ }, "execnet": { "hashes": [ - "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", - "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", + "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.1.1" }, "flake8": { "hashes": [ @@ -754,51 +859,51 @@ }, "fonttools": { "hashes": [ - "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", - "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37", - "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", - "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae", - "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b", - "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc", - "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", - "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", - "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", - "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", - "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", - "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6", - "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", - "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", - "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7", - "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6", - "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", - "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899", - "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50", - "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", - "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", - "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", - "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", - "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", - "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", - "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8", - "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506", - "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", - "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", - "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b", - "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", - "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c", - "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa", - "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", - "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", - "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", - "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", - "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946", - "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", - "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952", - "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", - "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8" + "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636", + "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce", + "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f", + "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1", + "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc", + "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f", + "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e", + "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716", + "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15", + "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77", + "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034", + "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba", + "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7", + "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55", + "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a", + "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0", + "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b", + "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671", + "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a", + "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039", + "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74", + "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836", + "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2", + "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308", + "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2", + "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5", + "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1", + "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438", + "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74", + "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f", + "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097", + "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e", + "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037", + "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1", + "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051", + "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b", + "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed", + "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68", + "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14", + "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5", + "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e", + "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936" ], "markers": "python_version >= '3.8'", - "version": "==4.47.2" + "version": "==4.51.0" }, "gprof2dot": { "hashes": [ @@ -819,11 +924,11 @@ }, "idna": { "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], "markers": "python_version >= '3.5'", - "version": "==3.6" + "version": "==3.7" }, "imagesize": { "hashes": [ @@ -835,11 +940,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e", - "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc" + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" ], "markers": "python_version >= '3.8'", - "version": "==7.0.1" + "version": "==7.1.0" }, "iniconfig": { "hashes": [ @@ -851,11 +956,35 @@ }, "jaraco.classes": { "hashes": [ - "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb", - "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621" + "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" ], "markers": "python_version >= '3.8'", - "version": "==3.3.0" + "version": "==3.4.0" + }, + "jaraco.context": { + "hashes": [ + "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266", + "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2" + ], + "markers": "python_version >= '3.8'", + "version": "==5.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:c279cb24c93d694ef7270f970d499cab4d3813f4e08273f95398651a634f0925", + "sha256:daf276ddf234bea897ef14f43c4e1bf9eefeac7b7a82a4dd69228ac20acff68d" + ], + "markers": "python_version >= '3.8'", + "version": "==4.0.0" + }, + "jeepney": { + "hashes": [ + "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" + ], + "markers": "sys_platform == 'linux'", + "version": "==0.8.0" }, "jinja2": { "hashes": [ @@ -866,13 +995,22 @@ "markers": "python_version >= '3.6'", "version": "==3.0.3" }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, "keyring": { "hashes": [ - "sha256:4446d35d636e6a10b8bce7caa66913dd9eca5fd222ca03a3d42c38608ac30836", - "sha256:e730ecffd309658a08ee82535a3b5ec4b4c8669a9be11efb66249d8e0aeb9a25" + "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427", + "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893" ], "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "version": "==25.1.0" }, "kiwisolver": { "hashes": [ @@ -994,103 +1132,103 @@ }, "markupsafe": { "hashes": [ - "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69", - "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0", - "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d", - "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec", - "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5", - "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411", - "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3", - "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74", - "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0", - "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949", - "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d", - "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279", - "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f", - "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6", - "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc", - "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e", - "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954", - "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656", - "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc", - "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518", - "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56", - "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc", - "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa", - "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565", - "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4", - "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb", - "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250", - "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4", - "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959", - "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc", - "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474", - "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863", - "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8", - "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f", - "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2", - "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e", - "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e", - "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb", - "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f", - "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a", - "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26", - "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d", - "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2", - "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131", - "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789", - "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6", - "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a", - "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858", - "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e", - "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb", - "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e", - "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84", - "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7", - "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea", - "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b", - "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6", - "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475", - "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74", - "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a", - "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.4" + "version": "==2.1.5" }, "matplotlib": { "hashes": [ - "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", - "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", - "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", - "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", - "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", - "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89", - "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", - "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", - "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", - "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", - "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", - "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", - "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843", - "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", - "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", - "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4", - "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", - "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", - "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b", - "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", - "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", - "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa", - "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917", - "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20", - "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", - "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", - "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f", - "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8" + "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67", + "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c", + "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94", + "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb", + "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9", + "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0", + "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616", + "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa", + "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661", + "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a", + "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae", + "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6", + "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea", + "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106", + "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef", + "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54", + "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f", + "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014", + "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338", + "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25", + "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b", + "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35", + "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732", + "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71", + "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10", + "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0", + "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30", + "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc" ], "markers": "python_version >= '3.9'", - "version": "==3.8.2" + "version": "==3.8.4" }, "mccabe": { "hashes": [ @@ -1169,24 +1307,24 @@ }, "nh3": { "hashes": [ - "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770", - "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf", - "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305", - "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601", - "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28", - "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7", - "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3", - "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911", - "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf", - "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0", - "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5", - "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97", - "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d", - "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e", - "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3", - "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6" - ], - "version": "==0.2.15" + "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a", + "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911", + "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb", + "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a", + "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc", + "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028", + "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9", + "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3", + "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351", + "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10", + "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71", + "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f", + "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b", + "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a", + "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062", + "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a" + ], + "version": "==0.2.17" }, "numpy": { "hashes": [ @@ -1222,13 +1360,18 @@ "index": "pypi", "version": "==0.9.1" }, + "opentrons-shared-data": { + "editable": true, + "markers": "python_version >= '3.8'", + "path": "../shared-data/python" + }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pathspec": { "hashes": [ @@ -1238,87 +1381,92 @@ "markers": "python_version >= '3.8'", "version": "==0.12.1" }, + "performance-metrics": { + "editable": true, + "file": "../performance-metrics" + }, "pillow": { "hashes": [ - "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8", - "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39", - "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac", - "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869", - "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e", - "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04", - "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9", - "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e", - "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe", - "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef", - "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56", - "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa", - "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f", - "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f", - "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e", - "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a", - "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2", - "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2", - "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5", - "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a", - "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2", - "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213", - "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563", - "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591", - "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c", - "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2", - "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb", - "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757", - "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0", - "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452", - "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad", - "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01", - "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f", - "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5", - "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61", - "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e", - "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b", - "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068", - "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9", - "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588", - "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483", - "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f", - "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67", - "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7", - "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311", - "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6", - "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72", - "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6", - "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129", - "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13", - "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67", - "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c", - "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516", - "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e", - "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e", - "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364", - "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023", - "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1", - "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04", - "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d", - "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a", - "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7", - "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb", - "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4", - "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e", - "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1", - "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48", - "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868" + "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", + "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2", + "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb", + "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d", + "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa", + "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3", + "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", + "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a", + "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", + "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8", + "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999", + "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599", + "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936", + "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375", + "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d", + "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", + "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60", + "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572", + "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", + "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced", + "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", + "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b", + "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", + "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f", + "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", + "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383", + "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", + "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355", + "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57", + "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", + "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b", + "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", + "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf", + "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f", + "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", + "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", + "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9", + "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", + "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45", + "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", + "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", + "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", + "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463", + "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", + "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591", + "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c", + "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd", + "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32", + "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9", + "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf", + "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5", + "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828", + "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3", + "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5", + "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2", + "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b", + "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2", + "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475", + "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3", + "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb", + "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", + "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015", + "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002", + "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170", + "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", + "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", + "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f", + "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", + "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a" ], "markers": "python_version >= '3.8'", - "version": "==10.2.0" + "version": "==10.3.0" }, "pkginfo": { "hashes": [ - "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546", - "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046" + "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" ], "markers": "python_version >= '3.6'", - "version": "==1.9.6" + "version": "==1.10.0" }, "platformdirs": { "hashes": [ @@ -1352,6 +1500,57 @@ "markers": "python_version >= '3.8'", "version": "==2.11.1" }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pydantic": { + "hashes": [ + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.10.12" + }, "pydocstyle": { "hashes": [ "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", @@ -1378,11 +1577,49 @@ }, "pyparsing": { "hashes": [ - "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", - "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db" + "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", + "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" ], "markers": "python_full_version >= '3.6.8'", - "version": "==3.1.1" + "version": "==3.1.2" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" }, "pytest": { "hashes": [ @@ -1395,12 +1632,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2", - "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef" + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.4" + "version": "==0.23.6" }, "pytest-cov": { "hashes": [ @@ -1448,19 +1685,19 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "readme-renderer": { "hashes": [ - "sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d", - "sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1" + "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311", + "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9" ], "markers": "python_version >= '3.8'", - "version": "==42.0" + "version": "==43.0" }, "requests": { "hashes": [ @@ -1488,11 +1725,19 @@ }, "rich": { "hashes": [ - "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", - "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" + "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", + "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.7.0" + "version": "==13.7.1" + }, + "secretstorage": { + "hashes": [ + "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + ], + "markers": "sys_platform == 'linux'", + "version": "==3.3.3" }, "six": { "hashes": [ @@ -1633,12 +1878,12 @@ }, "types-mock": { "hashes": [ - "sha256:13ca379d5710ccb3f18f69ade5b08881874cb83383d8fb49b1d4dac9d5c5d090", - "sha256:3d116955495935b0bcba14954b38d97e507cd43eca3e3700fc1b8e4f5c6bf2c7" + "sha256:0769cb376dfc75b45215619f17a9fd6333d771cc29ce4a38937f060b1e45530f", + "sha256:7472797986d83016f96fde7f73577d129b0cd8a8d0b783487a7be330d57ba431" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0.20240106" + "version": "==5.1.0.20240311" }, "types-setuptools": { "hashes": [ @@ -1650,19 +1895,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [ - "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20", - "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], "markers": "python_version >= '3.8'", - "version": "==2.2.0" + "version": "==2.2.1" }, "wheel": { "hashes": [ @@ -1675,11 +1920,11 @@ }, "zipp": { "hashes": [ - "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", - "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" ], "markers": "python_version >= '3.8'", - "version": "==3.17.0" + "version": "==3.18.1" } } } diff --git a/api/docs/v2/index.rst b/api/docs/v2/index.rst index 376d483f33b6..5e29296241de 100644 --- a/api/docs/v2/index.rst +++ b/api/docs/v2/index.rst @@ -171,17 +171,17 @@ More Resources Opentrons App +++++++++++++ -The `Opentrons App `__ is the easiest way to run your Python protocols. The app `supports `_ the latest versions of macOS, Windows, and Ubuntu. +The `Opentrons App `__ is the easiest way to run your Python protocols. The app runs on the latest versions of macOS, Windows, and Ubuntu. Support +++++++ -Questions about setting up your robot, using Opentrons software, or troubleshooting? Check out our `support articles `_ or `get in touch directly `_ with Opentrons Support. +Questions about setting up your robot, using Opentrons software, or troubleshooting? Check out our `support articles `_ or `contact Opentrons Support directly `_. Custom Protocol Service +++++++++++++++++++++++ -Don't have the time or resources to write your own protocols? The `Opentrons Custom Protocols `_ service can get you set up in as little as a week. +Don't have the time or resources to write your own protocols? Our `custom protocol development service `_ can get you set up in two weeks. Contributing ++++++++++++ diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index f05cd2e2f1ec..261d55e21003 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,32 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.1 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + +This release is primarily to unblock Flex runs. That fix is in + +### All changes + + + +--- + +## Internal Release 1.4.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + +--- + +## Internal Release 1.3.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + --- # Internal Release 1.1.0 diff --git a/api/release-notes.md b/api/release-notes.md index ff1932474595..ca9523121b4a 100644 --- a/api/release-notes.md +++ b/api/release-notes.md @@ -6,6 +6,21 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons Robot Software Changes in 7.2.2 + +Welcome to the v7.2.2 release of the Opentrons robot software! + +### Improved Features + +- Improved the low-volume performance of recently produced Flex 96-Channel Pipettes. + +### Bug Fixes + +- Restores the ability to use the speaker and camera on OT-2. +- Restores the ability to use the camera on Flex. + +--- + ## Opentrons Robot Software Changes in 7.2.1 Welcome to the v7.2.1 release of the Opentrons robot software! diff --git a/api/setup.py b/api/setup.py index ae53321ca223..1811b6b4e2dc 100755 --- a/api/setup.py +++ b/api/setup.py @@ -46,8 +46,6 @@ def get_version(): "Intended Audience :: Science/Research", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering", ] @@ -87,7 +85,7 @@ def read(*parts): if __name__ == "__main__": setup( - python_requires=">=3.8", + python_requires=">=3.10", name=DISTNAME, description=DESCRIPTION, license=LICENSE, diff --git a/api/src/opentrons/calibration_storage/deck_configuration.py b/api/src/opentrons/calibration_storage/deck_configuration.py index 31410403d35d..a627fce73c9c 100644 --- a/api/src/opentrons/calibration_storage/deck_configuration.py +++ b/api/src/opentrons/calibration_storage/deck_configuration.py @@ -10,6 +10,7 @@ class _CutoutFixturePlacementModel(pydantic.BaseModel): cutoutId: str cutoutFixtureId: str + opentronsModuleSerialNumber: Optional[str] class _DeckConfigurationModel(pydantic.BaseModel): @@ -26,7 +27,9 @@ def serialize_deck_configuration( data = _DeckConfigurationModel.construct( cutoutFixtures=[ _CutoutFixturePlacementModel.construct( - cutoutId=e.cutout_id, cutoutFixtureId=e.cutout_fixture_id + cutoutId=e.cutout_id, + cutoutFixtureId=e.cutout_fixture_id, + opentronsModuleSerialNumber=e.opentrons_module_serial_number, ) for e in cutout_fixture_placements ], @@ -50,7 +53,9 @@ def deserialize_deck_configuration( else: cutout_fixture_placements = [ CutoutFixturePlacement( - cutout_id=e.cutoutId, cutout_fixture_id=e.cutoutFixtureId + cutout_id=e.cutoutId, + cutout_fixture_id=e.cutoutFixtureId, + opentrons_module_serial_number=e.opentronsModuleSerialNumber, ) for e in parsed.cutoutFixtures ] diff --git a/api/src/opentrons/calibration_storage/types.py b/api/src/opentrons/calibration_storage/types.py index fd1bfbd5e2eb..bd80af337190 100644 --- a/api/src/opentrons/calibration_storage/types.py +++ b/api/src/opentrons/calibration_storage/types.py @@ -42,3 +42,4 @@ class UriDetails: class CutoutFixturePlacement: cutout_fixture_id: str cutout_id: str + opentrons_module_serial_number: typing.Optional[str] diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index a42a4f5f868e..42ca29a2b81e 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -28,6 +28,7 @@ ) from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.util.performance_helpers import track_analysis @click.command() @@ -63,6 +64,7 @@ def _get_input_files(files_and_dirs: Sequence[Path]) -> List[Path]: return results +@track_analysis async def _analyze( files_and_dirs: Sequence[Path], json_output: Optional[AsyncPath], diff --git a/api/src/opentrons/config/__init__.py b/api/src/opentrons/config/__init__.py index ce8676777779..a45715212110 100644 --- a/api/src/opentrons/config/__init__.py +++ b/api/src/opentrons/config/__init__.py @@ -284,6 +284,13 @@ class ConfigElement(NamedTuple): ConfigElementType.DIR, "The dir where module calibration is stored", ), + ConfigElement( + "performance_metrics_dir", + "Performance Metrics Directory", + Path("performance_metrics_data"), + ConfigElementType.DIR, + "The dir where performance metrics are stored", + ), ) #: The available configuration file elements to modify. All of these can be #: changed by editing opentrons.json, where the keys are the name elements, @@ -602,3 +609,7 @@ def get_tip_length_cal_path() -> Path: def get_custom_tiprack_def_path() -> Path: return get_opentrons_path("custom_tiprack_dir") + + +def get_performance_metrics_data_dir() -> Path: + return get_opentrons_path("performance_metrics_dir") diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index 191c0d69cccf..6a6076a84320 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -238,6 +238,16 @@ class Setting(NamedTuple): title="Enable OEM Mode", description="This setting anonymizes Opentrons branding in the ODD app.", robot_type=[RobotTypeEnum.FLEX], + ), + SettingDefinition( + _id="enablePerformanceMetrics", + title="Enable performance metrics", + description=( + "Do not enable." + " This is an Opentrons internal setting to collect performance metrics." + " Do not turn this on unless you are playing with the performance metrics system." + ), + robot_type=[RobotTypeEnum.OT2, RobotTypeEnum.FLEX], internal_only=True, ), ] @@ -709,6 +719,16 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: return newmap +def _migrate32to33(previous: SettingsMap) -> SettingsMap: + """Migrate to version 33 of the feature flags file. + + - Adds the enablePerformanceMetrics config element. + """ + newmap = {k: v for k, v in previous.items()} + newmap["enablePerformanceMetrics"] = None + return newmap + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -742,6 +762,7 @@ def _migrate31to32(previous: SettingsMap) -> SettingsMap: _migrate29to30, _migrate30to31, _migrate31to32, + _migrate32to33, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 4a1161a2391f..65984dd7ab9f 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -76,3 +76,11 @@ def enable_error_recovery_experiments() -> bool: return advs.get_setting_with_env_overload( "enableErrorRecoveryExperiments", RobotTypeEnum.FLEX ) + + +def enable_performance_metrics(robot_type: RobotTypeEnum) -> bool: + return advs.get_setting_with_env_overload("enablePerformanceMetrics", robot_type) + + +def oem_mode_enabled() -> bool: + return advs.get_setting_with_env_overload("enableOEMMode", RobotTypeEnum.FLEX) diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index a35f4a91d8de..e851d8a44f0b 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -28,7 +28,7 @@ from opentrons import protocol_api, __version__, should_use_ot3 -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.hardware_control import ( API as OT2API, @@ -333,7 +333,7 @@ def execute( # noqa: C901 'text': string_command_text, # The rest of this struct is # command-dependent; see - # opentrons.commands.commands. + # opentrons.legacy_commands.commands. } } diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 83439c0896b7..0edf7e4dfd35 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1647,3 +1647,8 @@ async def get_hepa_uv_state(self) -> Optional[HepaUVState]: if res else None ) + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + pass diff --git a/api/src/opentrons/hardware_control/backends/ot3simulator.py b/api/src/opentrons/hardware_control/backends/ot3simulator.py index 638b0094a85f..741018adc52f 100644 --- a/api/src/opentrons/hardware_control/backends/ot3simulator.py +++ b/api/src/opentrons/hardware_control/backends/ot3simulator.py @@ -506,13 +506,20 @@ def _attached_pipette_to_mount( ), "id": None, } - if found_model and expected_instr or found_model: + if found_model and init_instr["id"] is not None: # Instrument detected matches instrument expected (note: # "instrument detected" means passed as an argument to the # constructor of this class) # OR Instrument detected and no expected instrument specified - converted_name = pipette_load_name.convert_pipette_model(found_model) + + found_model_version = "" + if found_model.find("flex") > -1: + found_model = found_model.replace("_flex", "") # type: ignore + found_model_version = f"{init_instr['id'][4]}.{init_instr['id'][5]}" + converted_name = pipette_load_name.convert_pipette_model( + found_model, found_model_version + ) return { "config": load_pipette_data.load_definition( converted_name.pipette_type, @@ -843,3 +850,8 @@ async def set_hepa_uv_state(self, light_on: bool, timeout_s: int) -> bool: async def get_hepa_uv_state(self) -> Optional[HepaUVState]: return None + + def _update_tip_state(self, mount: OT3Mount, status: bool) -> None: + """This is something we only use in the simulator. + It is required so that PE simulations using ot3api don't break.""" + self._sim_tip_state[mount] = status diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 2d20a4f592ae..f8a9d48da60e 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -26,6 +26,11 @@ InvalidLiquidClassName, CommandPreconditionViolated, ) +from opentrons_shared_data.pipette.ul_per_mm import ( + piecewise_volume_conversion, + PIPETTING_FUNCTION_FALLBACK_VERSION, + PIPETTING_FUNCTION_LATEST_VERSION, +) from opentrons.types import Point, Mount @@ -33,11 +38,7 @@ from opentrons.config.types import RobotConfig from opentrons.drivers.types import MoveSplit from ..instrument_abc import AbstractInstrument -from ..instrument_helpers import ( - piecewise_volume_conversion, - PIPETTING_FUNCTION_FALLBACK_VERSION, - PIPETTING_FUNCTION_LATEST_VERSION, -) + from .instrument_calibration import ( PipetteOffsetByPipetteMount, load_pipette_offset, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py index 7e7352170b92..b7eae1aa1fcd 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/instrument_calibration.py @@ -21,7 +21,7 @@ ) from opentrons.hardware_control.types import OT3Mount -PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 1.5 +PIPETTE_OFFSET_CONSISTENCY_LIMIT: Final = 4.0 # These type aliases aid typechecking in tests that work the same on this and # the hardware_control.instruments.ot2 variant diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index b2dc7f01c020..7d72058d1ce7 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -25,12 +25,12 @@ CommandPreconditionViolated, PythonException, ) -from ..instrument_abc import AbstractInstrument -from ..instrument_helpers import ( +from opentrons_shared_data.pipette.ul_per_mm import ( piecewise_volume_conversion, PIPETTING_FUNCTION_FALLBACK_VERSION, PIPETTING_FUNCTION_LATEST_VERSION, ) +from ..instrument_abc import AbstractInstrument from .instrument_calibration import ( save_pipette_offset_calibration, load_pipette_offset, diff --git a/api/src/opentrons/hardware_control/modules/mod_abc.py b/api/src/opentrons/hardware_control/modules/mod_abc.py index c6ea41437ebf..9d5527991f63 100644 --- a/api/src/opentrons/hardware_control/modules/mod_abc.py +++ b/api/src/opentrons/hardware_control/modules/mod_abc.py @@ -2,9 +2,8 @@ import asyncio import logging import re -from pkg_resources import parse_version -from typing import ClassVar, Mapping, Optional, cast, TypeVar - +from typing import ClassVar, Mapping, Optional, TypeVar +from packaging.version import InvalidVersion, parse, Version from opentrons.config import IS_ROBOT, ROBOT_FIRMWARE_DIR from opentrons.drivers.rpi_drivers.types import USBPort @@ -16,6 +15,14 @@ TaskPayload = TypeVar("TaskPayload") +def parse_fw_version(version: str) -> Version: + try: + device_version = parse(version) + except InvalidVersion: + device_version = parse("v0.0.0") + return device_version + + class AbstractModule(abc.ABC): """Defines the common methods of a module.""" @@ -88,9 +95,9 @@ def get_bundled_fw(self) -> Optional[BundledFirmware]: def has_available_update(self) -> bool: """Return whether a newer firmware file is available""" if self.device_info and self._bundled_fw: - device_version = parse_version(self.device_info["version"]) - available_version = parse_version(self._bundled_fw.version) - return cast(bool, available_version > device_version) + device_version = parse_fw_version(self.device_info["version"]) + available_version = parse_fw_version(self._bundled_fw.version) + return available_version > device_version return False async def wait_for_is_running(self) -> None: diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 1a87d60d35e3..eb8054a87eed 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -64,6 +64,22 @@ def from_model(cls, model: ModuleModel) -> ModuleType: if isinstance(model, MagneticBlockModel): return cls.MAGNETIC_BLOCK + @classmethod + def to_module_fixture_id(cls, module_type: ModuleType) -> str: + if module_type == ModuleType.THERMOCYCLER: + # Thermocyclers are "loaded" in B1 only + return "thermocyclerModuleV2Front" + if module_type == ModuleType.TEMPERATURE: + return "temperatureModuleV2" + if module_type == ModuleType.HEATER_SHAKER: + return "heaterShakerModuleV1" + if module_type == ModuleType.MAGNETIC_BLOCK: + return "magneticBlockV1" + else: + raise ValueError( + f"Module Type {module_type} does not have a related fixture ID." + ) + class MagneticModuleModel(str, Enum): MAGNETIC_V1: str = "magneticModuleV1" diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index ae7be3396730..692d1f120e2e 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1521,8 +1521,14 @@ async def _home_axis(self, axis: Axis) -> None: # G, Q should be handled in the backend through `self._home()` assert axis not in [Axis.G, Axis.Q] + # TODO(CM): This is a temporary fix in response to the right mount causing + # errors while trying to home on startup or attachment. We should remove this + # when we fix this issue in the firmware. + enable_right_mount_on_startup = ( + self._gantry_load == GantryLoad.HIGH_THROUGHPUT and axis == Axis.Z_R + ) encoder_ok = self._backend.check_encoder_status([axis]) - if encoder_ok: + if encoder_ok or enable_right_mount_on_startup: # enable motor (if needed) and update estimation await self._enable_before_update_estimation(axis) @@ -1533,6 +1539,12 @@ async def _home_axis(self, axis: Axis) -> None: await self._set_plunger_current_and_home(axis, motor_ok, encoder_ok) return + # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable + # the motor when we are attempting to move. This should be already + # happening but something on the firmware is either not enabling the motor or + # disabling the motor. + await self.engage_axes([axis]) + # we can move to safe home distance! if encoder_ok and motor_ok: origin, target_pos = await self._retrieve_home_position(axis) @@ -1649,16 +1661,24 @@ async def retract_axis(self, axis: Axis) -> None: motor_ok = self._backend.check_motor_status([axis]) encoder_ok = self._backend.check_encoder_status([axis]) - if motor_ok and encoder_ok: - # we can move to the home position without checking the limit switch - origin = await self._backend.update_position() - target_pos = {axis: self._backend.home_position()[axis]} - await self._backend.move(origin, target_pos, 400, HWStopCondition.none) - else: - # home the axis - await self._home_axis(axis) - await self._cache_current_position() - await self._cache_encoder_position() + async with self._motion_lock: + if motor_ok and encoder_ok: + # TODO: (ba, 2024-04-19): We need to explictly engage the axis and enable + # the motor when we are attempting to move. This should be already + # happening but something on the firmware is either not enabling the motor or + # disabling the motor. + await self.engage_axes([axis]) + + # we can move to the home position without checking the limit switch + origin = await self._backend.update_position() + target_pos = {axis: self._backend.home_position()[axis]} + await self._backend.move(origin, target_pos, 400, HWStopCondition.none) + else: + # home the axis + await self._home_axis(axis) + + await self._cache_current_position() + await self._cache_encoder_position() # Gantry/frame (i.e. not pipette) config API @property @@ -2087,14 +2107,17 @@ async def _force_pick_up_tip( ) -> None: for press in pipette_spec.tip_action_moves: async with self._backend.motor_current(run_currents=press.currents): - target_down = target_position_from_relative( + target = target_position_from_relative( mount, top_types.Point(z=press.distance), self._current_position ) - await self._move(target_down, speed=press.speed, expect_stalls=True) - if press.distance < 0: - # we expect a stall has happened during a downward movement into the tiprack, so - # we want to update the motor estimation - await self._update_position_estimation([Axis.by_mount(mount)]) + if press.distance < 0: + # we expect a stall has happened during a downward movement into the tiprack, so + # we want to update the motor estimation + await self._move(target, speed=press.speed, expect_stalls=True) + await self._update_position_estimation([Axis.by_mount(mount)]) + else: + # we should not ignore stalls that happen during the retract part of the routine + await self._move(target, speed=press.speed, expect_stalls=False) async def _tip_motor_action( self, mount: OT3Mount, pipette_spec: List[TipActionMoveSpec] @@ -2133,6 +2156,8 @@ async def pick_up_tip( def add_tip_to_instr() -> None: instrument.add_tip(tip_length=tip_length) instrument.set_current_volume(0) + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, True) await self._move_to_plunger_bottom(realmount, rate=1.0) if ( @@ -2233,6 +2258,9 @@ def _remove_tips() -> None: await self._home([Axis.by_mount(mount)]) _remove_tips() + # call this in case we're simulating + if isinstance(self._backend, OT3Simulator): + self._backend._update_tip_state(realmount, False) async def clean_up(self) -> None: """Get the API ready to stop cleanly.""" diff --git a/api/src/opentrons/legacy_broker.py b/api/src/opentrons/legacy_broker.py index 838a75b7759d..b58a779134e1 100644 --- a/api/src/opentrons/legacy_broker.py +++ b/api/src/opentrons/legacy_broker.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, List from typing_extensions import Literal -from opentrons.commands import types +from opentrons.legacy_commands import types MODULE_LOG = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class LegacyBroker: Deprecated: Use the newer, more generic `opentrons.utils.Broker` class instead. - This class is coupled to old types from `opentrons.commands`. + This class is coupled to old types from `opentrons.legacy_commands`. https://opentrons.atlassian.net/browse/RSS-270 """ diff --git a/api/src/opentrons/legacy_commands/__init__.py b/api/src/opentrons/legacy_commands/__init__.py new file mode 100644 index 000000000000..558ad9b87c09 --- /dev/null +++ b/api/src/opentrons/legacy_commands/__init__.py @@ -0,0 +1 @@ +"""Command models from before v5.0, before Protocol Engine.""" diff --git a/api/src/opentrons/commands/commands.py b/api/src/opentrons/legacy_commands/commands.py similarity index 100% rename from api/src/opentrons/commands/commands.py rename to api/src/opentrons/legacy_commands/commands.py diff --git a/api/src/opentrons/commands/helpers.py b/api/src/opentrons/legacy_commands/helpers.py similarity index 100% rename from api/src/opentrons/commands/helpers.py rename to api/src/opentrons/legacy_commands/helpers.py diff --git a/api/src/opentrons/commands/module_commands.py b/api/src/opentrons/legacy_commands/module_commands.py similarity index 100% rename from api/src/opentrons/commands/module_commands.py rename to api/src/opentrons/legacy_commands/module_commands.py diff --git a/api/src/opentrons/commands/protocol_commands.py b/api/src/opentrons/legacy_commands/protocol_commands.py similarity index 100% rename from api/src/opentrons/commands/protocol_commands.py rename to api/src/opentrons/legacy_commands/protocol_commands.py diff --git a/api/src/opentrons/commands/publisher.py b/api/src/opentrons/legacy_commands/publisher.py similarity index 100% rename from api/src/opentrons/commands/publisher.py rename to api/src/opentrons/legacy_commands/publisher.py diff --git a/api/src/opentrons/commands/types.py b/api/src/opentrons/legacy_commands/types.py similarity index 100% rename from api/src/opentrons/commands/types.py rename to api/src/opentrons/legacy_commands/types.py diff --git a/api/src/opentrons/protocol_api/_parameter_context.py b/api/src/opentrons/protocol_api/_parameter_context.py index e16273b2a33e..8c9debd882ce 100644 --- a/api/src/opentrons/protocol_api/_parameter_context.py +++ b/api/src/opentrons/protocol_api/_parameter_context.py @@ -52,6 +52,7 @@ def add_int( description: A description of the parameter as it will show up on the frontend. unit: An optional unit to be appended to the end of the integer as it shown on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_int_parameter( display_name=display_name, variable_name=variable_name, @@ -88,13 +89,14 @@ def add_float( description: A description of the parameter as it will show up on the frontend. unit: An optional unit to be appended to the end of the float as it shown on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_float_parameter( display_name=display_name, variable_name=variable_name, - default=default, - minimum=minimum, - maximum=maximum, - choices=choices, + default=validation.ensure_float_value(default), + minimum=validation.ensure_optional_float_value(minimum), + maximum=validation.ensure_optional_float_value(maximum), + choices=validation.ensure_float_choices(choices), description=description, unit=unit, ) @@ -115,6 +117,7 @@ def add_bool( default: The default value the boolean parameter will be set to. This will be used in initial analysis. description: A description of the parameter as it will show up on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_bool_parameter( display_name=display_name, variable_name=variable_name, @@ -145,6 +148,7 @@ def add_str( Mutually exclusive with minimum and maximum. description: A description of the parameter as it will show up on the frontend. """ + validation.validate_variable_name_unique(variable_name, set(self._parameters)) parameter = parameter_definition.create_str_parameter( display_name=display_name, variable_name=variable_name, diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 6bf569bcd676..485f45d0e947 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -408,13 +408,18 @@ def pick_up_tip( well_name=well_name, well_location=well_location, ) - self._engine_client.pick_up_tip( + + self._engine_client.pick_up_tip_wait_for_recovery( pipette_id=self._pipette_id, labware_id=labware_id, well_name=well_name, well_location=well_location, ) + # Set the "last location" unconditionally, even if the command failed + # and was recovered from and we don't know if the pipette is physically here. + # This isn't used for path planning, but rather for implicit destination + # selection like in `pipette.aspirate(location=None)`. self._protocol_core.set_last_location(location=location, mount=self.get_mount()) def drop_tip( @@ -777,3 +782,8 @@ def configure_nozzle_layout( self._engine_client.configure_nozzle_layout( pipette_id=self._pipette_id, configuration_params=configuration_model ) + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) + self._engine_client.home([z_axis]) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index e3146a98a085..4089dff4b4d1 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -3,7 +3,7 @@ from typing import Dict, Optional, Type, Union, List, Tuple, TYPE_CHECKING from opentrons.protocol_engine.commands import LoadModuleResult -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.labware.dev_types import LabwareDefinition as LabwareDefDict from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -409,7 +409,6 @@ def load_module( robot_type = self._engine_client.state.config.robot_type normalized_deck_slot = deck_slot.to_equivalent_for_robot_type(robot_type) - self._ensure_module_location(normalized_deck_slot, module_type) result = self._engine_client.load_module( model=EngineModuleModel(model), @@ -602,7 +601,7 @@ def set_last_location( self._last_location = location self._last_mount = mount - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" return self._engine_client.state.labware.get_deck_definition() @@ -622,14 +621,6 @@ def get_staging_slot_definitions(self) -> Dict[str, SlotDefV3]: self._engine_client.state.addressable_areas.get_staging_slot_definitions() ) - def _ensure_module_location( - self, slot: DeckSlotName, module_type: ModuleType - ) -> None: - slot_def = self.get_slot_definition(slot) - compatible_modules = slot_def["compatibleModuleTypes"] - if module_type.value not in compatible_modules: - raise ValueError(f"A {module_type.value} cannot be loaded into slot {slot}") - def get_slot_item( self, slot_name: Union[DeckSlotName, StagingSlotName] ) -> Union[LabwareCore, ModuleCore, NonConnectedModuleCore, None]: diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 061e7d139601..fec252a009ee 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -289,6 +289,9 @@ def configure_nozzle_layout( @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" ... diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py index 9a9092af5aea..685f0f5d553e 100644 --- a/api/src/opentrons/protocol_api/core/legacy/deck.py +++ b/api/src/opentrons/protocol_api/core/legacy/deck.py @@ -280,6 +280,11 @@ def resolve_module_location( compatible_modules = slot_def["compatibleModuleTypes"] if module_type.value in compatible_modules: return location + elif ( + self._definition["robot"]["model"] == "OT-3 Standard" + and ModuleType.to_module_fixture_id(module_type) == slot_def["id"] + ): + return location else: raise ValueError( f"A {dn_from_type[module_type]} cannot be loaded" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 57f129c32b32..3755b093e78e 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -558,3 +558,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index d99c3032a718..02fc2003733d 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -1,7 +1,7 @@ import logging from typing import Dict, List, Optional, Set, Union, cast, Tuple -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.labware.dev_types import LabwareDefinition from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.robot.dev_types import RobotType @@ -491,7 +491,7 @@ def get_labware_on_labware( ) -> Optional[LegacyLabwareCore]: assert False, "get_labware_on_labware only supported on engine core" - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" assert False, "get_deck_definition only supported on engine core" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 2ee61adf24eb..ffcdda5019c2 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -476,3 +476,7 @@ def get_nozzle_map(self) -> NozzleMap: def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True + + def retract(self) -> None: + """Retract this instrument to the top of the gantry.""" + self._protocol_interface.get_hardware.retract(self._mount) # type: ignore [attr-defined] diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 8ed83388c073..a554c14e3063 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -5,7 +5,7 @@ from abc import abstractmethod, ABC from typing import Generic, List, Optional, Union, Tuple, Dict, TYPE_CHECKING -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.labware.dev_types import LabwareDefinition from opentrons_shared_data.robot.dev_types import RobotType @@ -188,7 +188,7 @@ def set_last_location( ... @abstractmethod - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the geometry definition of the robot's deck.""" @abstractmethod diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 1b58bcfc5242..e070b896a6ef 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -11,9 +11,9 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types -from opentrons.commands import commands as cmds +from opentrons.legacy_commands import commands as cmds -from opentrons.commands import publisher +from opentrons.legacy_commands import publisher from opentrons.protocols.advanced_control.mix import mix_from_kwargs from opentrons.protocols.advanced_control import transfers @@ -1532,6 +1532,12 @@ def move_to( return self + @requires_version(2, 18) + def _retract( + self, + ) -> None: + self._core.retract() + @property @requires_version(2, 0) def mount(self) -> str: diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f525fe6b3209..654a6ec46c14 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -8,8 +8,8 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.modules import ThermocyclerStep -from opentrons.commands import module_commands as cmds -from opentrons.commands.publisher import CommandPublisher, publish +from opentrons.legacy_commands import module_commands as cmds +from opentrons.legacy_commands.publisher import CommandPublisher, publish from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError, requires_version @@ -151,7 +151,7 @@ def load_labware( load_location = loaded_adapter._core else: load_location = self._core - + name = validation.ensure_lowercase_name(name) labware_core = self._protocol_core.load_labware( load_name=name, label=label, @@ -467,9 +467,9 @@ def engage( if height is not None: if self._api_version >= _MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN: raise APIVersionError( - "The height parameter of MagneticModuleContext.engage() was removed" - " in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." - " Use offset or height_from_base instead." + f"The height parameter of MagneticModuleContext.engage() was removed" + f" in {_MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN}." + f" Use offset or height_from_base instead." ) self._core.engage(height_from_home=height) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 2dd7815c09fa..feb8f56d91c0 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -20,9 +20,13 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.modules.types import MagneticBlockModel -from opentrons.commands import protocol_commands as cmds, types as cmd_types -from opentrons.commands.helpers import stringify_labware_movement_command -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands import protocol_commands as cmds, types as cmd_types +from opentrons.legacy_commands.helpers import stringify_labware_movement_command +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.deck_type import ( NoTrashDefinedError, diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index f714f35cecdf..eb72c6b6dfdf 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -87,6 +87,10 @@ class InvalidTrashBinLocationError(ValueError): """An error raised when attempting to load trash bins in invalid slots.""" +class InvalidFixtureLocationError(ValueError): + """An error raised when attempting to load a fixture in an invalid cutout.""" + + def ensure_mount_for_pipette( mount: Union[str, Mount, None], pipette: PipetteNameType ) -> Mount: diff --git a/api/src/opentrons/protocol_engine/actions/__init__.py b/api/src/opentrons/protocol_engine/actions/__init__.py index b1181e6a50e9..ac3fc653976e 100644 --- a/api/src/opentrons/protocol_engine/actions/__init__.py +++ b/api/src/opentrons/protocol_engine/actions/__init__.py @@ -11,6 +11,7 @@ PauseAction, PauseSource, StopAction, + ResumeFromRecoveryAction, FinishAction, HardwareStoppedAction, QueueCommandAction, @@ -38,6 +39,7 @@ "PlayAction", "PauseAction", "StopAction", + "ResumeFromRecoveryAction", "FinishAction", "HardwareStoppedAction", "QueueCommandAction", diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index d5c6bb49abc9..2d46f614ec3d 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -55,10 +55,7 @@ class PauseAction: @dataclass(frozen=True) class StopAction: - """Stop the current engine execution. - - After a StopAction, the engine status will be marked as stopped. - """ + """Request engine execution to stop soon.""" from_estop: bool = False @@ -154,11 +151,32 @@ class FailCommandAction: """ command_id: str + """The command to fail.""" + error_id: str + """An ID to assign to the command's error. + + Must be unique to this occurrence of the error. + """ + failed_at: datetime + """When the command failed.""" + error: EnumeratedError + """The underlying exception that caused this command to fail.""" + notes: List[CommandNote] + """Overwrite the command's `.notes` with these.""" + type: ErrorRecoveryType + """How this error should be handled in the context of the overall run.""" + + # This is a quick hack so FailCommandAction handlers can get the params of the + # command that failed. We probably want this to be a new "failure details" + # object instead, similar to how succeeded commands can send a "private result" + # to Protocol Engine internals. + running_command: Command + """The command to fail, in its prior `running` state.""" @dataclass(frozen=True) diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 53703c16dee2..f95611c1b4c7 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -6,7 +6,9 @@ from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition -from opentrons.commands.protocol_commands import comment as make_legacy_comment_command +from opentrons.legacy_commands.protocol_commands import ( + comment as make_legacy_comment_command, +) from opentrons.types import MountType from opentrons.hardware_control.modules.types import ThermocyclerStep @@ -294,6 +296,29 @@ def pick_up_tip( return cast(commands.PickUpTipResult, result) + def pick_up_tip_wait_for_recovery( + self, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: WellLocation, + ) -> commands.PickUpTip: + """Execute a PickUpTip, wait for any error recovery, and return it. + + Note that the returned command will not necessarily have a `result`. + """ + request = commands.PickUpTipCreate( + params=commands.PickUpTipParams( + pipetteId=pipette_id, + labwareId=labware_id, + wellName=well_name, + wellLocation=well_location, + ) + ) + command = self._transport.execute_command_wait_for_recovery(request=request) + + return cast(commands.PickUpTip, command) + def drop_tip( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/clients/transports.py b/api/src/opentrons/protocol_engine/clients/transports.py index 270599ff4699..6de08db97edc 100644 --- a/api/src/opentrons/protocol_engine/clients/transports.py +++ b/api/src/opentrons/protocol_engine/clients/transports.py @@ -1,15 +1,28 @@ """A helper for controlling a `ProtocolEngine` without async/await.""" from asyncio import AbstractEventLoop, run_coroutine_threadsafe -from typing import Any, overload +from typing import Any, Final, overload from typing_extensions import Literal from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.labware.labware_definition import LabwareDefinition + from ..protocol_engine import ProtocolEngine from ..errors import ProtocolCommandFailedError +from ..error_recovery_policy import ErrorRecoveryType from ..state import StateView -from ..commands import CommandCreate, CommandResult +from ..commands import Command, CommandCreate, CommandResult, CommandStatus + + +class RunStoppedBeforeCommandError(RuntimeError): + """Raised if the ProtocolEngine was stopped before a command could start.""" + + def __init__(self, command: Command) -> None: + self._command = command + super().__init__( + f"The run was stopped" + f" before {command.commandType} command {command.id} could execute." + ) class ChildThreadTransport: @@ -30,8 +43,10 @@ def __init__(self, engine: ProtocolEngine, loop: AbstractEventLoop) -> None: want to synchronously access it. loop: The event loop that `engine` is running in (in the other thread). """ - self._engine = engine - self._loop = loop + # We might access these from different threads, + # so let's make them Final for (shallow) immutability. + self._engine: Final = engine + self._loop: Final = loop @property def state(self) -> StateView: @@ -39,7 +54,11 @@ def state(self) -> StateView: return self._engine.state_view def execute_command(self, request: CommandCreate) -> CommandResult: - """Execute a ProtocolEngine command, blocking until the command completes. + """Execute a ProtocolEngine command. + + This blocks until the command completes. If the command fails, this will always + raise the failure as an exception--even if ProtocolEngine deemed the failure + recoverable. Args: request: The ProtocolEngine command request @@ -48,8 +67,11 @@ def execute_command(self, request: CommandCreate) -> CommandResult: The command's result data. Raises: - ProtocolEngineError: if the command execution is not successful, - the specific error that cause the command to fail is raised. + ProtocolEngineError: If the command execution was not successful, + the specific error that caused the command to fail is raised. + + If the run was stopped before the command could complete, that's + also signaled as this exception. """ command = run_coroutine_threadsafe( self._engine.add_and_execute_command(request=request), @@ -64,21 +86,76 @@ def execute_command(self, request: CommandCreate) -> CommandResult: message=f"{error.errorType}: {error.detail}", ) - # FIXME(mm, 2023-04-10): This assert can easily trigger from this sequence: - # - # 1. The engine is paused. - # 2. The user's Python script calls this method to start a new command, - # which remains `queued` because of the pause. - # 3. The engine is stopped. - # - # The returned command will be `queued`, so it won't have a result. - # - # We need to figure out a proper way to report this condition to callers - # so they correctly interpret it as an intentional stop, not an internal error. - assert command.result is not None, f"Expected Command {command} to have result" + if command.result is None: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued` + # and won't have a result. + raise RunStoppedBeforeCommandError(command) return command.result + def execute_command_wait_for_recovery(self, request: CommandCreate) -> Command: + """Execute a ProtocolEngine command, including error recovery. + + This blocks until the command completes. Additionally, if the command fails, + this will continue to block until its error recovery has been completed. + + Args: + request: The ProtocolEngine command request. + + Returns: + The command. If error recovery happened for it, the command will be + reported here as failed. + + Raises: + ProtocolEngineError: If the command failed, *and* the failure was not + recovered from. + + If the run was stopped before the command could complete, that's + also signalled as this exception. + """ + + async def run_in_pe_thread() -> Command: + command = await self._engine.add_and_execute_command_wait_for_recovery( + request=request + ) + + if command.error is not None: + error_was_recovered_from = ( + self._engine.state_view.commands.get_error_recovery_type(command.id) + == ErrorRecoveryType.WAIT_FOR_RECOVERY + ) + if not error_was_recovered_from: + error = command.error + # TODO: this needs to have an actual code + raise ProtocolCommandFailedError( + original_error=error, + message=f"{error.errorType}: {error.detail}", + ) + + elif command.status == CommandStatus.QUEUED: + # This can happen with a certain pause timing: + # + # 1. The engine is paused. + # 2. The user's Python script calls this method to start a new command, + # which remains `queued` because of the pause. + # 3. The engine is stopped. The returned command will be `queued`, + # and won't have a result. + raise RunStoppedBeforeCommandError(command) + + return command + + command = run_coroutine_threadsafe( + run_in_pe_thread(), + loop=self._loop, + ).result() + + return command + @overload def call_method( self, diff --git a/api/src/opentrons/protocol_engine/commands/load_module.py b/api/src/opentrons/protocol_engine/commands/load_module.py index 1d877d089412..5c1d474be4da 100644 --- a/api/src/opentrons/protocol_engine/commands/load_module.py +++ b/api/src/opentrons/protocol_engine/commands/load_module.py @@ -5,7 +5,15 @@ from pydantic import BaseModel, Field from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate -from ..types import DeckSlotLocation, ModuleModel, ModuleDefinition +from ..types import ( + DeckSlotLocation, + ModuleType, + ModuleModel, + ModuleDefinition, +) +from opentrons.types import DeckSlotName + +from opentrons.protocol_engine.resources import deck_configuration_provider if TYPE_CHECKING: from ..state import StateView @@ -104,9 +112,22 @@ def __init__( async def execute(self, params: LoadModuleParams) -> LoadModuleResult: """Check that the requested module is attached and assign its identifier.""" - self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - params.location.slotName.id - ) + module_type = params.model.as_type() + self._ensure_module_location(params.location.slotName, module_type) + + if self._state_view.config.robot_type == "OT-2 Standard": + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.location.slotName.id + ) + else: + addressable_area = self._state_view.geometry._modules.ensure_and_convert_module_fixture_location( + deck_slot=params.location.slotName, + deck_type=self._state_view.config.deck_type, + model=params.model, + ) + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + addressable_area + ) verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location @@ -132,6 +153,30 @@ async def execute(self, params: LoadModuleParams) -> LoadModuleResult: definition=loaded_module.definition, ) + def _ensure_module_location( + self, slot: DeckSlotName, module_type: ModuleType + ) -> None: + if self._state_view.config.robot_type == "OT-2 Standard": + slot_def = self._state_view.addressable_areas.get_slot_definition(slot.id) + compatible_modules = slot_def["compatibleModuleTypes"] + if module_type.value not in compatible_modules: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + else: + cutout_fixture_id = ModuleType.to_module_fixture_id(module_type) + module_fixture = deck_configuration_provider.get_cutout_fixture( + cutout_fixture_id, + self._state_view.addressable_areas.state.deck_definition, + ) + cutout_id = ( + self._state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot) + ) + if cutout_id not in module_fixture["mayMountTo"]: + raise ValueError( + f"A {module_type.value} cannot be loaded into slot {slot}" + ) + class LoadModule(BaseCommand[LoadModuleParams, LoadModuleResult]): """The model for a load module command.""" diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index 39268f28bc7c..ab91b5fabaa9 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -20,6 +20,7 @@ async def create_protocol_engine( config: Config, load_fixed_trash: bool = False, deck_configuration: typing.Optional[DeckConfigurationType] = None, + notify_publishers: typing.Optional[typing.Callable[[], None]] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. @@ -28,6 +29,7 @@ async def create_protocol_engine( config: ProtocolEngine configuration. load_fixed_trash: Automatically load fixed trash labware in engine. deck_configuration: The initial deck configuration the engine will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ deck_data = DeckDataProvider(config.deck_type) deck_definition = await deck_data.get_deck_definition() @@ -45,6 +47,7 @@ async def create_protocol_engine( is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) return ProtocolEngine(state_store=state_store, hardware_api=hardware_api) diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 9d9ff99b33e4..0e27a270c94e 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -951,16 +951,18 @@ def __init__( class EStopActivatedError(ProtocolEngineError): - """Raised when an operation's required pipette tip is not attached.""" + """Represents an E-stop event.""" def __init__( self, - message: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an EStopActivatedError.""" - super().__init__(ErrorCodes.E_STOP_ACTIVATED, message, details, wrapping) + super().__init__( + code=ErrorCodes.E_STOP_ACTIVATED, + message="E-stop activated.", + wrapping=wrapping, + ) class NotSupportedOnRobotType(ProtocolEngineError): diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index d44d37f5641c..d00b5c0a96d9 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -159,7 +159,7 @@ async def execute(self, command_id: str) -> None: if isinstance(error, asyncio.CancelledError): error = RunStoppedError("Run was cancelled") elif isinstance(error, EStopActivatedError): - error = PE_EStopActivatedError(message=str(error), wrapping=[error]) + error = PE_EStopActivatedError(wrapping=[error]) elif not isinstance(error, EnumeratedError): error = PythonException(error) @@ -167,6 +167,7 @@ async def execute(self, command_id: str) -> None: FailCommandAction( error=error, command_id=running_command.id, + running_command=running_command, error_id=self._model_utils.generate_id(), failed_at=self._model_utils.get_timestamp(), notes=note_tracker.get_notes(), diff --git a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py index 3323aab0aa37..8b59eda5ef2f 100644 --- a/api/src/opentrons/protocol_engine/execution/create_queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/create_queue_worker.py @@ -39,7 +39,6 @@ def create_queue_worker( equipment_handler = EquipmentHandler( hardware_api=hardware_api, state_store=state_store, - action_dispatcher=action_dispatcher, ) movement_handler = MovementHandler( diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 2487ad50aaae..ee04653bda28 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -1,6 +1,6 @@ """Equipment command side-effect logic.""" from dataclasses import dataclass -from typing import Optional, overload +from typing import Optional, overload, Union from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -22,7 +22,6 @@ TemperatureModuleId, ThermocyclerModuleId, ) -from ..actions import ActionDispatcher from ..errors import ( FailedToLoadPipetteError, LabwareDefinitionDoesNotExistError, @@ -44,6 +43,7 @@ LabwareOffsetLocation, ModuleModel, ModuleDefinition, + AddressableAreaLocation, ) @@ -98,7 +98,6 @@ def __init__( self, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, labware_data_provider: Optional[LabwareDataProvider] = None, module_data_provider: Optional[ModuleDataProvider] = None, model_utils: Optional[ModelUtils] = None, @@ -109,7 +108,6 @@ def __init__( """Initialize an EquipmentHandler instance.""" self._hardware_api = hardware_api self._state_store = state_store - self._action_dispatcher = action_dispatcher self._labware_data_provider = labware_data_provider or LabwareDataProvider() self._module_data_provider = module_data_provider or ModuleDataProvider() self._model_utils = model_utils or ModelUtils() @@ -252,7 +250,7 @@ async def load_pipette( async def load_magnetic_block( self, model: ModuleModel, - location: DeckSlotLocation, + location: Union[DeckSlotLocation, AddressableAreaLocation], module_id: Optional[str], ) -> LoadedModuleData: """Ensure the required magnetic block is attached. @@ -317,10 +315,14 @@ async def load_module( for hw_mod in self._hardware_api.attached_modules ] + serial_number_at_locaiton = self._state_store.geometry._addressable_areas.get_fixture_serial_from_deck_configuration_by_deck_slot( + location.slotName + ) attached_module = self._state_store.modules.select_hardware_module_to_load( model=model, location=location, attached_modules=attached_modules, + expected_serial_number=serial_number_at_locaiton, ) else: diff --git a/api/src/opentrons/protocol_engine/execution/queue_worker.py b/api/src/opentrons/protocol_engine/execution/queue_worker.py index c1ba60eb1437..179880c03e92 100644 --- a/api/src/opentrons/protocol_engine/execution/queue_worker.py +++ b/api/src/opentrons/protocol_engine/execution/queue_worker.py @@ -72,6 +72,9 @@ async def _run_commands(self) -> None: command_id = await self._state_store.wait_for( condition=self._state_store.commands.get_next_to_execute ) + # Assert for type hinting. This is valid because the wait_for() above + # only returns when the value is truthy. + assert command_id is not None except RunStoppedError: # There are no more commands that we should execute, either because the run has # completed on its own, or because a client requested it to stop. diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 8e23c08013f1..8bb4c91dda3c 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -5,7 +5,6 @@ from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction from opentrons.protocol_engine.error_recovery_policy import ( ErrorRecoveryPolicy, - ErrorRecoveryType, error_recovery_by_ff, ) @@ -58,7 +57,6 @@ HardwareStoppedAction, ResetTipsAction, SetPipetteMovementSpeedAction, - FailCommandAction, ) @@ -159,8 +157,12 @@ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> No else: self._hardware_api.resume(HardwarePauseType.PAUSE) - def pause(self) -> None: - """Pause executing commands in the queue.""" + def request_pause(self) -> None: + """Make command execution pause soon. + + This will try to pause in the middle of the ongoing command, if there is one. + Otherwise, whenever the next command begins, the pause will happen then. + """ action = self._state_store.commands.validate_action_allowed( PauseAction(source=PauseSource.CLIENT) ) @@ -234,7 +236,10 @@ async def add_and_execute_command( the command in state. Returns: - The command. If the command completed, it will be succeeded or failed. + The command. + + If the command completed, it will be succeeded or failed. + If the engine was stopped before it reached the command, the command will be queued. """ @@ -242,80 +247,85 @@ async def add_and_execute_command( await self.wait_for_command(command.id) return self._state_store.commands.get(command.id) - def estop( - self, - # TODO(mm, 2024-03-26): Maintenance runs are a robot-server concept that - # ProtocolEngine should not have to know about. Can this be simplified or - # defined in other terms? - maintenance_run: bool, - ) -> None: - """Signal to the engine that an estop event occurred. - - If there are any queued commands for the engine, they will be marked - as failed due to the estop event. If there aren't any queued commands - *and* this is a maintenance run (which has commands queued one-by-one), - a series of actions will mark the engine as Stopped. In either case the - queue worker will be deactivated; the primary difference is that the former - case will expect the protocol runner to `finish()` the engine, whereas the - maintenance run will be put into a state wherein the engine can be discarded. - """ - if self._state_store.commands.get_is_stopped(): - return + async def add_and_execute_command_wait_for_recovery( + self, request: commands.CommandCreate + ) -> commands.Command: + """Like `add_and_execute_command()`, except wait for error recovery. + + Unlike `add_and_execute_command()`, if the command fails, this will not + immediately return the failed command. Instead, if the error is recoverable, + it will wait until error recovery has completed (e.g. when some other task + calls `self.resume_from_recovery()`). + + Returns: + The command. - current_id = ( - self._state_store.commands.get_running_command_id() - or self._state_store.commands.get_queue_ids().head(None) + If the command completed, it will be succeeded or failed. If it failed + and then its failure was recovered from, it will still be failed. + + If the engine was stopped before it reached the command, + the command will be queued. + """ + queued_command = self.add_command(request) + await self.wait_for_command(command_id=queued_command.id) + completed_command = self._state_store.commands.get(queued_command.id) + await self._state_store.wait_for_not( + self.state_view.commands.get_recovery_in_progress_for_command, + queued_command.id, ) + return completed_command - if current_id is not None: - fail_action = FailCommandAction( - command_id=current_id, - error_id=self._model_utils.generate_id(), - failed_at=self._model_utils.get_timestamp(), - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - self._action_dispatcher.dispatch(fail_action) - - # In the case where the running command was a setup command - check if there - # are any pending *run* commands and, if so, clear them all - current_id = self._state_store.commands.get_queue_ids().head(None) - if current_id is not None: - fail_action = FailCommandAction( - command_id=current_id, - error_id=self._model_utils.generate_id(), - failed_at=self._model_utils.get_timestamp(), - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - self._action_dispatcher.dispatch(fail_action) - self._queue_worker.cancel() - elif maintenance_run: - stop_action = self._state_store.commands.validate_action_allowed( + def estop(self) -> None: + """Signal to the engine that an E-stop event occurred. + + If an estop happens while the robot is moving, lower layers physically stop + motion and raise the event as an exception, which fails the Protocol Engine + command. No action from the `ProtocolEngine` caller is needed to handle that. + + However, if an estop happens in between commands, or in the middle of + a command like `comment` or `waitForDuration` that doesn't access the hardware, + `ProtocolEngine` needs to be told about it so it can interrupt the command + and stop executing any more. This method is how to do that. + + This acts roughly like `request_stop()`. After calling this, you should call + `finish()` with an EStopActivatedError. + """ + try: + action = self._state_store.commands.validate_action_allowed( StopAction(from_estop=True) ) - self._action_dispatcher.dispatch(stop_action) - hardware_stop_action = HardwareStoppedAction( - completed_at=self._model_utils.get_timestamp(), - finish_error_details=FinishErrorDetails( - error=EStopActivatedError(message="Estop Activated"), - error_id=self._model_utils.generate_id(), - created_at=self._model_utils.get_timestamp(), - ), + except Exception: # todo(mm, 2024-04-16): Catch a more specific type. + # This is likely called from some hardware API callback that doesn't care + # about ProtocolEngine lifecycle or what methods are valid to call at what + # times. So it makes more sense for us to no-op here than to propagate this + # as an error. + _log.info( + "ProtocolEngine cannot handle E-stop event right now. Ignoring it.", + exc_info=True, ) - self._action_dispatcher.dispatch(hardware_stop_action) - self._queue_worker.cancel() - else: - _log.info("estop pressed before protocol was started, taking no action.") + return + self._action_dispatcher.dispatch(action) + # self._queue_worker.cancel() will try to interrupt any ongoing command. + # Unfortunately, if it's a hardware command, this interruption will race + # against the E-stop exception propagating up from lower layers. But we need to + # do this because we want to make sure non-hardware commands, like + # `waitForDuration`, are also interrupted. + self._queue_worker.cancel() + # Unlike self.request_stop(), we don't need to do + # self._hardware_api.cancel_execution_and_running_tasks(). Since this was an + # E-stop event, the hardware API already knows. + + async def request_stop(self) -> None: + """Make command execution stop soon. - async def stop(self) -> None: - """Stop execution immediately, halting all motion and cancelling future commands. + This will try to interrupt the ongoing command, if there is one. Future commands + are canceled. However, by the time this method returns, things may not have + settled by the time this method returns; the last command may still be + running. - After an engine has been `stop`'ed, it cannot be restarted. + After a stop has been requested, the engine cannot be restarted. - After a `stop`, you must still call `finish` to give the engine a chance + After a stop request, you must still call `finish` to give the engine a chance to clean up resources and propagate errors. """ action = self._state_store.commands.validate_action_allowed(StopAction()) @@ -348,14 +358,20 @@ async def finish( set_run_status: bool = True, post_run_hardware_state: PostRunHardwareState = PostRunHardwareState.HOME_AND_STAY_ENGAGED, ) -> None: - """Gracefully finish using the ProtocolEngine, waiting for it to become idle. + """Finish using the `ProtocolEngine`. + + This does a few things: + + 1. It may do post-run actions like homing and dropping tips. This depends on the + arguments passed as well as heuristics based on the history of the engine. + 2. It waits for the engine to be done controlling the robot's hardware. + 3. It releases internal resources, like background tasks. - The engine will finish executing its current command (if any), - and then shut down. After an engine has been `finished`'ed, it cannot - be restarted. + It's safe to call `finish()` multiple times. After you call `finish()`, + the engine can't be restarted. This method should not raise. If any exceptions happened during execution that were not - properly caught by the CommandExecutor, or if any exceptions happen during this + properly caught by `ProtocolEngine` internals, or if any exceptions happen during this `finish()` call, they should be saved as `.state_view.get_summary().errors`. Arguments: @@ -369,12 +385,11 @@ async def finish( if self._state_store.commands.state.stopped_by_estop: # This handles the case where the E-stop was pressed while we were *not* in the middle # of some hardware interaction that would raise it as an exception. For example, imagine - # we were paused between two commands, or imagine we were executing a very long run of - # comment commands. + # we were paused between two commands, or imagine we were executing a waitForDuration. drop_tips_after_run = False post_run_hardware_state = PostRunHardwareState.DISENGAGE_IN_PLACE if error is None: - error = EStopActivatedError(message="Estop was activated during a run") + error = EStopActivatedError() if error: # If the run had an error, check if that error indicates an E-stop. diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index 112be3663cd9..648bd4f4484b 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,7 +1,7 @@ """Deck configuration resource provider.""" from typing import List, Set, Tuple -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, CutoutFixture from opentrons.types import DeckSlotName @@ -17,11 +17,10 @@ CutoutDoesNotExistError, FixtureDoesNotExistError, AddressableAreaDoesNotExistError, - FixtureDoesNotProvideAreasError, ) -def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint: +def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV5) -> DeckPoint: """Get the base position of a cutout on the deck.""" for cutout in deck_definition["locations"]["cutouts"]: if cutout_id == cutout["id"]: @@ -32,7 +31,7 @@ def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> De def get_cutout_fixture( - cutout_fixture_id: str, deck_definition: DeckDefinitionV4 + cutout_fixture_id: str, deck_definition: DeckDefinitionV5 ) -> CutoutFixture: """Gets cutout fixture from deck that matches the cutout fixture ID provided.""" for cutout_fixture in deck_definition["cutoutFixtures"]: @@ -44,20 +43,18 @@ def get_cutout_fixture( def get_provided_addressable_area_names( - cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4 + cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV5 ) -> List[str]: """Gets a list of the addressable areas provided by the cutout fixture on the cutout.""" cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition) try: return cutout_fixture["providesAddressableAreas"][cutout_id] - except KeyError as exception: - raise FixtureDoesNotProvideAreasError( - f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}" - ) from exception + except KeyError: + return [] def get_addressable_area_display_name( - addressable_area_name: str, deck_definition: DeckDefinitionV4 + addressable_area_name: str, deck_definition: DeckDefinitionV5 ) -> str: """Get the display name for an addressable area name.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: @@ -69,7 +66,7 @@ def get_addressable_area_display_name( def get_potential_cutout_fixtures( - addressable_area_name: str, deck_definition: DeckDefinitionV4 + addressable_area_name: str, deck_definition: DeckDefinitionV5 ) -> Tuple[str, Set[PotentialCutoutFixture]]: """Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures.""" potential_fixtures = [] @@ -102,7 +99,7 @@ def get_addressable_area_from_name( addressable_area_name: str, cutout_position: DeckPoint, base_slot: DeckSlotName, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> AddressableArea: """Given a name and a cutout position, get an addressable area on the deck.""" for addressable_area in deck_definition["locations"]["addressableAreas"]: diff --git a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py index 6098c2f4301d..017fc58f5527 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_data_provider.py @@ -9,7 +9,7 @@ load as load_deck, DEFAULT_DECK_DEFINITION_VERSION, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -39,10 +39,10 @@ def __init__( self._deck_type = deck_type self._labware_data = labware_data or LabwareDataProvider() - async def get_deck_definition(self) -> DeckDefinitionV4: + async def get_deck_definition(self) -> DeckDefinitionV5: """Get a labware definition given the labware's identification.""" - def sync() -> DeckDefinitionV4: + def sync() -> DeckDefinitionV5: return load_deck( name=self._deck_type.value, version=DEFAULT_DECK_DEFINITION_VERSION ) @@ -51,7 +51,7 @@ def sync() -> DeckDefinitionV4: async def get_deck_fixed_labware( self, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> List[DeckFixedLabware]: """Get a list of all labware fixtures from a given deck definition.""" labware: List[DeckFixedLabware] = [] diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 04894fe33384..909beffbe860 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -4,7 +4,7 @@ from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.deck.dev_types import ( - DeckDefinitionV4, + DeckDefinitionV5, SlotDefV3, CutoutFixture, ) @@ -56,7 +56,7 @@ class AddressableAreaState: potential_cutout_fixtures_by_cutout_id: Dict[str, Set[PotentialCutoutFixture]] - deck_definition: DeckDefinitionV4 + deck_definition: DeckDefinitionV5 deck_configuration: Optional[DeckConfigurationType] """The host robot's full deck configuration. @@ -94,7 +94,7 @@ class AddressableAreaState: def _get_conflicting_addressable_areas_error_string( potential_cutout_fixtures: Set[PotentialCutoutFixture], loaded_addressable_areas: Dict[str, AddressableArea], - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> str: loaded_areas_on_cutout = set() for fixture in potential_cutout_fixtures: @@ -158,7 +158,7 @@ def __init__( self, deck_configuration: DeckConfigurationType, config: Config, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, ) -> None: """Initialize an addressable area store and its state.""" if config.use_simulated_deck_config: @@ -224,11 +224,11 @@ def _handle_command(self, command: Command) -> None: @staticmethod def _get_addressable_areas_from_deck_configuration( - deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV4 + deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV5 ) -> Dict[str, AddressableArea]: """Return all addressable areas provided by the given deck configuration.""" addressable_areas = [] - for cutout_id, cutout_fixture_id in deck_config: + for cutout_id, cutout_fixture_id, opentrons_module_serial_number in deck_config: provided_addressable_areas = ( deck_configuration_provider.get_provided_addressable_area_names( cutout_fixture_id, cutout_id, deck_definition @@ -351,7 +351,7 @@ def get_all_cutout_fixtures(self) -> Optional[List[str]]: assert self._state.deck_configuration is not None return [ cutout_fixture_id - for _, cutout_fixture_id in self._state.deck_configuration + for _, cutout_fixture_id, _serial in self._state.deck_configuration ] def _get_loaded_addressable_area( @@ -453,11 +453,31 @@ def get_addressable_area_position( """ addressable_area = self._get_addressable_area_from_deck_data( addressable_area_name=addressable_area_name, - do_compatibility_check=do_compatibility_check, + do_compatibility_check=False, # This should probably not default to false ) position = addressable_area.position return Point(x=position.x, y=position.y, z=position.z) + def get_addressable_area_offsets_from_cutout( + self, + addressable_area_name: str, + ) -> Point: + """Get the offset form cutout fixture of an addressable area.""" + for addressable_area in self.state.deck_definition["locations"][ + "addressableAreas" + ]: + if addressable_area["id"] == addressable_area_name: + area_offset = addressable_area["offsetFromCutoutFixture"] + position = Point( + x=area_offset[0], + y=area_offset[1], + z=area_offset[2], + ) + return Point(x=position.x, y=position.y, z=position.z) + raise ValueError( + f"No matching addressable area named {addressable_area_name} identified." + ) + def get_addressable_area_bounding_box( self, addressable_area_name: str, @@ -499,6 +519,10 @@ def get_addressable_area_center(self, addressable_area_name: str) -> Point: z=position.z, ) + def get_cutout_id_by_deck_slot_name(self, slot_name: DeckSlotName) -> str: + """Get the Cutout ID of a given Deck Slot by Deck Slot Name.""" + return DECK_SLOT_TO_CUTOUT_MAP[slot_name] + def get_fixture_by_deck_slot_name( self, slot_name: DeckSlotName ) -> Optional[CutoutFixture]: @@ -508,7 +532,11 @@ def get_fixture_by_deck_slot_name( slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] slot_cutout_fixture = None # This will only ever be one under current assumptions - for cutout_id, cutout_fixture_id in deck_config: + for ( + cutout_id, + cutout_fixture_id, + opentrons_module_serial_number, + ) in deck_config: if cutout_id == slot_cutout_id: slot_cutout_fixture = ( deck_configuration_provider.get_cutout_fixture( @@ -532,6 +560,23 @@ def get_fixture_height(self, cutout_fixture_name: str) -> float: ) return cutout_fixture["height"] + def get_fixture_serial_from_deck_configuration_by_deck_slot( + self, slot_name: DeckSlotName + ) -> Optional[str]: + """Get the serial number provided by the deck configuration for a Fixture at a given location.""" + deck_config = self.state.deck_configuration + if deck_config: + slot_cutout_id = DECK_SLOT_TO_CUTOUT_MAP[slot_name] + # This will only ever be one under current assumptions + for ( + cutout_id, + cutout_fixture_id, + opentrons_module_serial_number, + ) in deck_config: + if cutout_id == slot_cutout_id: + return opentrons_module_serial_number + return None + def get_slot_definition(self, slot_id: str) -> SlotDefV3: """Get the definition of a slot in the deck. diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index ab4d3b8f5cbb..b58052510463 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -4,7 +4,7 @@ import enum from dataclasses import dataclass from datetime import datetime -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from typing_extensions import assert_never from opentrons_shared_data.errors import EnumeratedError, ErrorCodes, PythonException @@ -164,6 +164,22 @@ class CommandState: # that we're doing error recovery. See if we can implement robot-server pagination # atop simpler concepts, like "the last command that ran" or "the next command that # would run." + # + # TODO(mm, 2024-04-03): Can this be replaced by + # CommandHistory.get_terminal_command() now? + + command_error_recovery_types: Dict[str, ErrorRecoveryType] + """For each command that failed (indexed by ID), what its recovery type was. + + This only includes commands that actually failed, not the ones that we mark as + failed but that are effectively "cancelled" because a command before them failed. + + This separate attribute is a stopgap until error recovery concepts are a bit more + stable. Eventually, we might want this info to be stored directly on each command. + """ + + recovery_target_command_id: Optional[str] + """If we're currently recovering from a command failure, which command it was.""" finish_error: Optional[ErrorOccurrence] """The error that happened during the post-run finish steps (homing & dropping tips), if any.""" @@ -199,6 +215,8 @@ def __init__( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_completed_at=None, run_started_at=None, latest_command_hash=None, @@ -253,11 +271,11 @@ def handle_action(self, action: Action) -> None: # noqa: C901 error=action.error, ) - # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=action.command_id, failed_at=action.failed_at, error_occurrence=error_occurrence, + error_recovery_type=action.type, notes=action.notes, ) @@ -271,10 +289,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 self._state.command_history.get_setup_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_setup_queue() @@ -284,15 +304,18 @@ def handle_action(self, action: Action) -> None: # noqa: C901 ): if action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY: self._state.queue_status = QueueStatus.AWAITING_RECOVERY + self._state.recovery_target_command_id = action.command_id elif action.type == ErrorRecoveryType.FAIL_RUN: other_command_ids_to_fail = ( self._state.command_history.get_queue_ids() ) for command_id in other_command_ids_to_fail: + # TODO(mc, 2022-06-06): add new "cancelled" status or similar self._update_to_failed( command_id=command_id, failed_at=action.failed_at, error_occurrence=None, + error_recovery_type=None, notes=None, ) self._state.command_history.clear_queue() @@ -317,13 +340,16 @@ def handle_action(self, action: Action) -> None: # noqa: C901 elif isinstance(action, ResumeFromRecoveryAction): self._state.queue_status = QueueStatus.RUNNING + self._state.recovery_target_command_id = None elif isinstance(action, StopAction): if not self._state.run_result: self._state.queue_status = QueueStatus.PAUSED - self._state.run_result = RunResult.STOPPED if action.from_estop: self._state.stopped_by_estop = True + self._state.run_result = RunResult.FAILED + else: + self._state.run_result = RunResult.STOPPED elif isinstance(action, FinishAction): if not self._state.run_result: @@ -337,12 +363,12 @@ def handle_action(self, action: Action) -> None: # noqa: C901 else: self._state.run_result = RunResult.STOPPED - if action.error_details: - self._state.run_error = self._map_run_exception_to_error_occurrence( - action.error_details.error_id, - action.error_details.created_at, - action.error_details.error, - ) + if not self._state.run_error and action.error_details: + self._state.run_error = self._map_run_exception_to_error_occurrence( + action.error_details.error_id, + action.error_details.created_at, + action.error_details.error, + ) elif isinstance(action, HardwareStoppedAction): self._state.queue_status = QueueStatus.PAUSED @@ -376,6 +402,7 @@ def _update_to_failed( command_id: str, failed_at: datetime, error_occurrence: Optional[ErrorOccurrence], + error_recovery_type: Optional[ErrorRecoveryType], notes: Optional[List[CommandNote]], ) -> None: prev_entry = self._state.command_history.get(command_id) @@ -391,6 +418,8 @@ def _update_to_failed( } ) self._state.command_history.set_command_failed(failed_command) + if error_recovery_type is not None: + self._state.command_error_recovery_types[command_id] = error_recovery_type @staticmethod def _map_run_exception_to_error_occurrence( @@ -687,6 +716,10 @@ def get_all_commands_final(self) -> bool: return no_command_running and no_command_to_execute + def get_recovery_in_progress_for_command(self, command_id: str) -> bool: + """Return whether the given command failed and its error recovery is in progress.""" + return self._state.recovery_target_command_id == command_id + def raise_fatal_command_error(self) -> None: """Raise the run's fatal command error, if there was one, as an exception. @@ -709,6 +742,13 @@ def raise_fatal_command_error(self) -> None: message=failed_command.command.error.detail, ) + def get_error_recovery_type(self, command_id: str) -> ErrorRecoveryType: + """Return the error recovery type with which the given command failed. + + The command ID is assumed to point to a failed command. + """ + return self.state.command_error_recovery_types[command_id] + def get_is_stopped(self) -> bool: """Get whether an engine stop has completed.""" return self._state.run_completed_at is not None diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 1822881eea2b..4a37bf798c17 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -166,6 +166,7 @@ def get_highest_z_in_slot( except LabwareNotLoadedOnModuleError: return self._modules.get_module_highest_z( module_id=module_id, + addressable_areas=self._addressable_areas, ) else: return self.get_highest_z_of_labware_stack(labware_id) @@ -246,7 +247,9 @@ def _get_labware_position_offset( return LabwareOffsetVector(x=0, y=0, z=0) elif isinstance(labware_location, ModuleLocation): module_id = labware_location.moduleId - module_offset = self._modules.get_nominal_module_offset(module_id=module_id) + module_offset = self._modules.get_nominal_module_offset( + module_id=module_id, addressable_areas=self._addressable_areas + ) module_model = self._modules.get_connected_model(module_id) stacking_overlap = self._labware.get_module_overlap_offsets( labware_id, module_model diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 7709410fd0fd..a11f1a58e4a7 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -15,7 +15,7 @@ Union, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE from opentrons_shared_data.labware.labware_definition import LabwareRole from opentrons_shared_data.pipette.dev_types import LabwareUri @@ -106,7 +106,7 @@ class LabwareState: labware_offsets_by_id: Dict[str, LabwareOffset] definitions_by_uri: Dict[str, LabwareDefinition] - deck_definition: DeckDefinitionV4 + deck_definition: DeckDefinitionV5 class LabwareStore(HasState[LabwareState], HandlesActions): @@ -116,7 +116,7 @@ class LabwareStore(HasState[LabwareState], HandlesActions): def __init__( self, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], ) -> None: """Initialize a labware store and its state.""" @@ -324,7 +324,7 @@ def get_display_name(self, labware_id: str) -> str: or self.get_definition(labware_id).metadata.displayName ) - def get_deck_definition(self) -> DeckDefinitionV4: + def get_deck_definition(self) -> DeckDefinitionV5: """Get the current deck definition.""" return self._state.deck_definition diff --git a/api/src/opentrons/protocol_engine/state/modules.py b/api/src/opentrons/protocol_engine/state/modules.py index 84093de0d4a7..0e79dd53cf25 100644 --- a/api/src/opentrons/protocol_engine/state/modules.py +++ b/api/src/opentrons/protocol_engine/state/modules.py @@ -46,6 +46,7 @@ DeckType, LabwareMovementOffsetData, ) +from .addressable_areas import AddressableAreaView from .. import errors from ..commands import ( Command, @@ -210,11 +211,12 @@ def handle_action(self, action: Action) -> None: def _handle_command(self, command: Command) -> None: if isinstance(command.result, LoadModuleResult): + slot_name = command.params.location.slotName self._add_module_substate( module_id=command.result.moduleId, serial_number=command.result.serialNumber, definition=command.result.definition, - slot_name=command.params.location.slotName, + slot_name=slot_name, requested_model=command.params.model, module_live_data=None, ) @@ -707,35 +709,70 @@ def get_dimensions(self, module_id: str) -> ModuleDimensions: def get_nominal_module_offset( self, module_id: str, + addressable_areas: AddressableAreaView, ) -> LabwareOffsetVector: """Get the module's nominal offset vector computed with slot transform.""" - definition = self.get_definition(module_id) - slot = self.get_location(module_id).slotName.id - - pre_transform: NDArray[npdouble] = array( - ( - definition.labwareOffset.x, - definition.labwareOffset.y, - definition.labwareOffset.z, - 1, + if ( + self.state.deck_type == DeckType.OT2_STANDARD + or self.state.deck_type == DeckType.OT2_SHORT_TRASH + ): + definition = self.get_definition(module_id) + slot = self.get_location(module_id).slotName.id + + pre_transform: NDArray[npdouble] = array( + ( + definition.labwareOffset.x, + definition.labwareOffset.y, + definition.labwareOffset.z, + 1, + ) + ) + xforms_ser = definition.slotTransforms.get( + str(self._state.deck_type.value), {} + ).get( + slot, + { + "labwareOffset": [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ] + }, + ) + xforms_ser_offset = xforms_ser["labwareOffset"] + + # Apply the slot transform, if any + xform: NDArray[npdouble] = array(xforms_ser_offset) + xformed = dot(xform, pre_transform) + return LabwareOffsetVector( + x=xformed[0], + y=xformed[1], + z=xformed[2], + ) + else: + module = self.get(module_id) + if isinstance(module.location, DeckSlotLocation): + location = module.location.slotName + elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2: + location = DeckSlotName.SLOT_B1 + else: + raise ValueError( + "Module location invalid for nominal module offset calculation." + ) + module_addressable_area = self.ensure_and_convert_module_fixture_location( + location, self.state.deck_type, module.model + ) + module_addressable_area_position = ( + addressable_areas.get_addressable_area_offsets_from_cutout( + module_addressable_area + ) + ) + return LabwareOffsetVector( + x=module_addressable_area_position.x, + y=module_addressable_area_position.y, + z=module_addressable_area_position.z, ) - ) - xforms_ser = definition.slotTransforms.get( - str(self._state.deck_type.value), {} - ).get( - slot, - {"labwareOffset": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]}, - ) - xforms_ser_offset = xforms_ser["labwareOffset"] - - # Apply the slot transform, if any - xform: NDArray[npdouble] = array(xforms_ser_offset) - xformed = dot(xform, pre_transform) - return LabwareOffsetVector( - x=xformed[0], - y=xformed[1], - z=xformed[2], - ) def get_module_calibration_offset( self, module_id: str @@ -755,7 +792,9 @@ def get_height_over_labware(self, module_id: str) -> float: """Get the height of module parts above module labware base.""" return self.get_dimensions(module_id).overLabwareHeight - def get_module_highest_z(self, module_id: str) -> float: + def get_module_highest_z( + self, module_id: str, addressable_areas: AddressableAreaView + ) -> float: """Get the highest z point of the module, as placed on the robot. The highest Z of a module, unlike the bare overall height, depends on @@ -781,7 +820,7 @@ def get_module_highest_z(self, module_id: str) -> float: z_difference = module_height - default_lw_offset_point nominal_transformed_lw_offset_z = self.get_nominal_module_offset( - module_id=module_id + module_id=module_id, addressable_areas=addressable_areas ).z calibration_offset = self.get_module_calibration_offset(module_id) return ( @@ -943,11 +982,12 @@ def is_edge_move_unsafe(self, mount: MountType, target_slot: DeckSlotName) -> bo return neighbor_slot in self._state.slot_by_module_id.values() - def select_hardware_module_to_load( + def select_hardware_module_to_load( # noqa: C901 self, model: ModuleModel, location: DeckSlotLocation, attached_modules: Sequence[HardwareModule], + expected_serial_number: Optional[str] = None, ) -> HardwareModule: """Get the next matching hardware module for the given model and location. @@ -963,6 +1003,8 @@ def select_hardware_module_to_load( location: The location the module will be assigned to. attached_modules: All attached modules as reported by the HardwareAPI, in the order in which they should be used. + expected_serial_number: An optional variable containing the serial number + expected of the module identified. Raises: ModuleNotAttachedError: A not-yet-assigned module matching the requested @@ -976,7 +1018,6 @@ def select_hardware_module_to_load( if slot == location.slotName: existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id) break - if existing_mod_in_slot: existing_def = existing_mod_in_slot.definition @@ -992,7 +1033,11 @@ def select_hardware_module_to_load( for m in attached_modules: if m not in self._state.hardware_by_module_id.values(): if model == m.definition.model or model in m.definition.compatibleWith: - return m + if expected_serial_number is not None: + if m.serial_number == expected_serial_number: + return m + else: + return m raise errors.ModuleNotAttachedError(f"No available {model.value} found.") @@ -1063,3 +1108,92 @@ def is_flex_deck_with_thermocycler(self) -> bool: return True else: return False + + def ensure_and_convert_module_fixture_location( + self, + deck_slot: DeckSlotName, + deck_type: DeckType, + model: ModuleModel, + ) -> str: + """Ensure module fixture load location is valid. + + Also, convert the deck slot to a valid module fixture addressable area. + """ + if deck_type == DeckType.OT2_STANDARD or deck_type == DeckType.OT2_SHORT_TRASH: + raise ValueError( + f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures." + ) + + if model == ModuleModel.MAGNETIC_BLOCK_V1: + valid_slots = [ + slot + for slot in [ + "A1", + "B1", + "C1", + "D1", + "A2", + "B2", + "C2", + "D2", + "A3", + "B3", + "C3", + "D3", + ] + ] + addressable_areas = [ + "magneticBlockV1A1", + "magneticBlockV1B1", + "magneticBlockV1C1", + "magneticBlockV1D1", + "magneticBlockV1A2", + "magneticBlockV1B2", + "magneticBlockV1C2", + "magneticBlockV1D2", + "magneticBlockV1A3", + "magneticBlockV1B3", + "magneticBlockV1C3", + "magneticBlockV1D3", + ] + + elif model == ModuleModel.HEATER_SHAKER_MODULE_V1: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "heaterShakerV1A1", + "heaterShakerV1B1", + "heaterShakerV1C1", + "heaterShakerV1D1", + "heaterShakerV1A3", + "heaterShakerV1B3", + "heaterShakerV1C3", + "heaterShakerV1D3", + ] + elif model == ModuleModel.TEMPERATURE_MODULE_V2: + valid_slots = [ + slot for slot in ["A1", "B1", "C1", "D1", "A3", "B3", "C3", "D3"] + ] + addressable_areas = [ + "temperatureModuleV2A1", + "temperatureModuleV2B1", + "temperatureModuleV2C1", + "temperatureModuleV2D1", + "temperatureModuleV2A3", + "temperatureModuleV2B3", + "temperatureModuleV2C3", + "temperatureModuleV2D3", + ] + elif model == ModuleModel.THERMOCYCLER_MODULE_V2: + return "thermocyclerModuleV2" + else: + raise ValueError( + f"Unknown module {model.name} has no addressable areas to provide." + ) + + map_addressable_area = { + slot: addressable_area + for slot, addressable_area in zip(valid_slots, addressable_areas) + } + return map_addressable_area[deck_slot.value] diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index a34f016deab9..dcde17a78942 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -2,10 +2,10 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial -from typing import Any, Callable, Dict, List, Optional, Sequence, TypeVar +from typing import Callable, Dict, List, Optional, Sequence, TypeVar +from typing_extensions import ParamSpec -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocol_engine.types import ModuleOffsetData @@ -30,7 +30,9 @@ from .state_summary import StateSummary from ..types import DeckConfigurationType -ReturnT = TypeVar("ReturnT") + +_ParamsT = ParamSpec("_ParamsT") +_ReturnT = TypeVar("_ReturnT") @dataclass(frozen=True) @@ -140,12 +142,13 @@ def __init__( self, *, config: Config, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, deck_fixed_labware: Sequence[DeckFixedLabware], is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, deck_configuration: Optional[DeckConfigurationType] = None, + notify_publishers: Optional[Callable[[], None]] = None, ) -> None: """Initialize a StateStore and its substores. @@ -159,6 +162,7 @@ def __init__( change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. deck_configuration: The initial deck configuration the addressable area store will be instantiated with. + notify_publishers: Notifies robot server publishers of internal state change. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) self._pipette_store = PipetteStore() @@ -191,6 +195,7 @@ def __init__( ] self._config = config self._change_notifier = change_notifier or ChangeNotifier() + self._notify_robot_server = notify_publishers self._initialize_state() def handle_action(self, action: Action) -> None: @@ -207,10 +212,10 @@ def handle_action(self, action: Action) -> None: async def wait_for( self, - condition: Callable[..., Optional[ReturnT]], - *args: Any, - **kwargs: Any, - ) -> ReturnT: + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: """Wait for a condition to become true, checking whenever state changes. If the condition is already true, return immediately. @@ -255,14 +260,43 @@ async def wait_for( Raises: The exception raised by the `condition` function, if any. """ - predicate = partial(condition, *args, **kwargs) - is_done = predicate() - while not is_done: + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=True) + + async def wait_for_not( + self, + condition: Callable[_ParamsT, _ReturnT], + *args: _ParamsT.args, + **kwargs: _ParamsT.kwargs, + ) -> _ReturnT: + """Like `wait_for()`, except wait for the condition to become false. + + See the documentation in `wait_for()`, especially the warning about condition + design. + + The advantage of having this separate method over just passing a wrapper lambda + as the condition to `wait_for()` yourself is that wrapper lambdas are hard to + test in the mock-heavy Decoy + Protocol Engine style. + """ + + def predicate() -> _ReturnT: + return condition(*args, **kwargs) + + return await self._wait_for(condition=predicate, truthiness_to_wait_for=False) + + async def _wait_for( + self, condition: Callable[[], _ReturnT], truthiness_to_wait_for: bool + ) -> _ReturnT: + current_value = condition() + + while bool(current_value) != truthiness_to_wait_for: await self._change_notifier.wait() - is_done = predicate() + current_value = condition() - return is_done + return current_value def _get_next_state(self) -> State: """Get a new instance of the state value object.""" @@ -319,3 +353,5 @@ def _update_state_views(self) -> None: self._liquid._state = next_state.liquids self._tips._state = next_state.tips self._change_notifier.notify() + if self._notify_robot_server is not None: + self._notify_robot_server() diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index a2539ff45e71..f5d68d61ee56 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -7,11 +7,13 @@ from ..actions import ( Action, SucceedCommandAction, + FailCommandAction, ResetTipsAction, ) from ..commands import ( Command, LoadLabwareResult, + PickUpTip, PickUpTipResult, DropTipResult, DropTipInPlaceResult, @@ -20,6 +22,7 @@ PipetteConfigUpdateResultMixin, PipetteNozzleLayoutResultMixin, ) +from ..error_recovery_policy import ErrorRecoveryType from opentrons.hardware_control.nozzle_manager import NozzleMap @@ -71,7 +74,7 @@ def handle_action(self, action: Action) -> None: self._state.channels_by_pipette_id[pipette_id] = config.channels self._state.active_channels_by_pipette_id[pipette_id] = config.channels self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map - self._handle_command(action.command) + self._handle_succeeded_command(action.command) if isinstance(action.private_result, PipetteNozzleLayoutResultMixin): pipette_id = action.private_result.pipette_id @@ -86,6 +89,9 @@ def handle_action(self, action: Action) -> None: pipette_id ] = self._state.channels_by_pipette_id[pipette_id] + elif isinstance(action, FailCommandAction): + self._handle_failed_command(action) + elif isinstance(action, ResetTipsAction): labware_id = action.labware_id @@ -94,7 +100,7 @@ def handle_action(self, action: Action) -> None: well_name ] = TipRackWellState.CLEAN - def _handle_command(self, command: Command) -> None: + def _handle_succeeded_command(self, command: Command) -> None: if ( isinstance(command.result, LoadLabwareResult) and command.result.definition.parameters.isTiprack @@ -124,6 +130,28 @@ def _handle_command(self, command: Command) -> None: pipette_id = command.params.pipetteId self._state.length_by_pipette_id.pop(pipette_id, None) + def _handle_failed_command( + self, + action: FailCommandAction, + ) -> None: + # If a pickUpTip command fails recoverably, mark the tips as used. This way, + # when the protocol is resumed and the Python Protocol API calls + # `get_next_tip()`, we'll move on to other tips as expected. + # + # We don't attempt this for nonrecoverable errors because maybe the failure + # was due to a bad labware ID or well name. + if ( + isinstance(action.running_command, PickUpTip) + and action.type != ErrorRecoveryType.FAIL_RUN + ): + self._set_used_tips( + pipette_id=action.running_command.params.pipetteId, + labware_id=action.running_command.params.labwareId, + well_name=action.running_command.params.wellName, + ) + # Note: We're logically removing the tip from the tip rack, + # but we're not logically updating the pipette to have that tip on it. + def _set_used_tips( # noqa: C901 self, pipette_id: str, well_name: str, labware_id: str ) -> None: diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 266dc6aa81ff..d7b0e981b2af 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -714,6 +714,10 @@ class AreaType(Enum): MOVABLE_TRASH = "movableTrash" FIXED_TRASH = "fixedTrash" WASTE_CHUTE = "wasteChute" + THERMOCYCLER = "thermocycler" + HEATER_SHAKER = "heaterShaker" + TEMPERATURE = "temperatureModule" + MAGNETICBLOCK = "magneticBlock" @dataclass(frozen=True) @@ -820,7 +824,10 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ] # TODO make the below some sort of better type -DeckConfigurationType = List[Tuple[str, str]] # cutout_id, cutout_fixture_id +# TODO This should instead contain a proper cutout fixture type +DeckConfigurationType = List[ + Tuple[str, str, Optional[str]] +] # cutout_id, cutout_fixture_id, opentrons_module_serial_number class TipPresenceStatus(str, Enum): @@ -847,6 +854,7 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": }[state] +# TODO (spp, 2024-04-02): move all RTP types to runner class RTPBase(BaseModel): """Parameters defined in a protocol.""" diff --git a/api/src/opentrons/protocol_runner/legacy_command_mapper.py b/api/src/opentrons/protocol_runner/legacy_command_mapper.py index 53846baf653b..e835a6af8e65 100644 --- a/api/src/opentrons/protocol_runner/legacy_command_mapper.py +++ b/api/src/opentrons/protocol_runner/legacy_command_mapper.py @@ -6,7 +6,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType, DeckSlotName, Location -from opentrons.commands import types as legacy_command_types +from opentrons.legacy_commands import types as legacy_command_types from opentrons.protocol_engine import ( ProtocolEngineError, actions as pe_actions, @@ -265,6 +265,7 @@ def map_command( # noqa: C901 results.append( pe_actions.FailCommandAction( command_id=running_command.id, + running_command=running_command, error_id=ModelUtils.generate_id(), failed_at=now, error=LegacyContextCommandError(command_error), diff --git a/api/src/opentrons/protocol_runner/legacy_context_plugin.py b/api/src/opentrons/protocol_runner/legacy_context_plugin.py index 3e32877f2323..7dd882f0fb71 100644 --- a/api/src/opentrons/protocol_runner/legacy_context_plugin.py +++ b/api/src/opentrons/protocol_runner/legacy_context_plugin.py @@ -5,7 +5,7 @@ from contextlib import ExitStack from typing import List, Optional -from opentrons.commands.types import CommandMessage as LegacyCommand +from opentrons.legacy_commands.types import CommandMessage as LegacyCommand from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions from opentrons.util.broker import ReadOnlyBroker diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 67ea3d15db4c..9c097bbba2de 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -36,6 +36,7 @@ LegacyExecutor, LegacyLoadInfo, ) +from ..protocol_engine.errors import ProtocolCommandFailedError from ..protocol_engine.types import ( PostRunHardwareState, DeckConfigurationType, @@ -100,12 +101,12 @@ def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> No def pause(self) -> None: """Pause the run.""" - self._protocol_engine.pause() + self._protocol_engine.request_pause() async def stop(self) -> None: """Stop (cancel) the run.""" if self.was_started(): - await self._protocol_engine.stop() + await self._protocol_engine.request_stop() else: await self._protocol_engine.finish( drop_tips_after_run=False, @@ -283,6 +284,7 @@ def __init__( ) self._hardware_api.should_taskify_movement_execution(taskify=False) + self._queued_commands: List[pe_commands.CommandCreate] = [] async def load(self, protocol_source: ProtocolSource) -> None: """Load a JSONv6+ ProtocolSource into managed ProtocolEngine.""" @@ -324,17 +326,16 @@ async def load(self, protocol_source: ProtocolSource) -> None: color=liquid.displayColor, ) await _yield() + initial_home_command = pe_commands.HomeCreate( params=pe_commands.HomeParams(axes=None) ) # this command homes all axes, including pipette plugner and gripper jaw self._protocol_engine.add_command(request=initial_home_command) - for command in commands: - self._protocol_engine.add_command(request=command) - await _yield() + self._queued_commands = commands - self._task_queue.set_run_func(func=self._protocol_engine.wait_until_complete) + self._task_queue.set_run_func(func=self._add_command_and_execute) async def run( # noqa: D102 self, @@ -355,6 +356,15 @@ async def run( # noqa: D102 commands = self._protocol_engine.state_view.commands.get_all() return RunResult(commands=commands, state_summary=run_data, parameters=[]) + async def _add_command_and_execute(self) -> None: + for command in self._queued_commands: + result = await self._protocol_engine.add_and_execute_command(command) + if result and result.error: + raise ProtocolCommandFailedError( + original_error=result.error, + message=f"{result.error.errorType}: {result.error.detail}", + ) + class LiveRunner(AbstractRunner): """Protocol runner implementation for live http protocols.""" diff --git a/api/src/opentrons/protocols/duration/estimator.py b/api/src/opentrons/protocols/duration/estimator.py index 6f481c297725..5e3b6ef26633 100644 --- a/api/src/opentrons/protocols/duration/estimator.py +++ b/api/src/opentrons/protocols/duration/estimator.py @@ -7,7 +7,7 @@ from dataclasses import dataclass -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocols.api_support.deck_type import ( guess_from_global_config as guess_deck_type_from_global_config, ) diff --git a/api/src/opentrons/protocols/parameters/validation.py b/api/src/opentrons/protocols/parameters/validation.py index cbb2464ebd0e..8e7a0bed8ad6 100644 --- a/api/src/opentrons/protocols/parameters/validation.py +++ b/api/src/opentrons/protocols/parameters/validation.py @@ -1,5 +1,5 @@ import keyword -from typing import List, Optional, Union, Literal +from typing import List, Set, Optional, Union, Literal from .types import ( AllowedTypes, @@ -16,17 +16,34 @@ DESCRIPTION_MAX_LEN = 100 +def validate_variable_name_unique( + variable_name: str, other_variable_names: Set[str] +) -> None: + """Validate that the given variable name is unique.""" + if isinstance(variable_name, str) and variable_name in other_variable_names: + raise ParameterNameError( + f'"{variable_name}" is already defined as a variable name for another parameter.' + f" All variable names must be unique." + ) + + def ensure_display_name(display_name: str) -> str: """Validate display name is within the character limit.""" + if not isinstance(display_name, str): + raise ParameterNameError( + f"Display name must be a string and at most {DISPLAY_NAME_MAX_LEN} characters." + ) if len(display_name) > DISPLAY_NAME_MAX_LEN: raise ParameterNameError( - f"Display name {display_name} greater than {DISPLAY_NAME_MAX_LEN} characters." + f'Display name "{display_name}" greater than {DISPLAY_NAME_MAX_LEN} characters.' ) return display_name def ensure_variable_name(variable_name: str) -> str: """Validate variable name is a valid python variable name.""" + if not isinstance(variable_name, str): + raise ParameterNameError("Variable name must be a string.") if not variable_name.isidentifier(): raise ParameterNameError( "Variable name must only contain alphanumeric characters, underscores, and cannot start with a digit." @@ -38,19 +55,29 @@ def ensure_variable_name(variable_name: str) -> str: def ensure_description(description: Optional[str]) -> Optional[str]: """Validate description is within the character limit.""" - if description is not None and len(description) > DESCRIPTION_MAX_LEN: - raise ParameterNameError( - f"Description {description} greater than {DESCRIPTION_MAX_LEN} characters." - ) + if description is not None: + if not isinstance(description, str): + raise ParameterNameError( + f"Description must be a string and at most {DESCRIPTION_MAX_LEN} characters." + ) + if len(description) > DESCRIPTION_MAX_LEN: + raise ParameterNameError( + f'Description "{description}" greater than {DESCRIPTION_MAX_LEN} characters.' + ) return description def ensure_unit_string_length(unit: Optional[str]) -> Optional[str]: """Validate unit is within the character limit.""" - if unit is not None and len(unit) > UNIT_MAX_LEN: - raise ParameterNameError( - f"Description {unit} greater than {UNIT_MAX_LEN} characters." - ) + if unit is not None: + if not isinstance(unit, str): + raise ParameterNameError( + f"Unit must be a string and at most {UNIT_MAX_LEN} characters." + ) + if len(unit) > UNIT_MAX_LEN: + raise ParameterNameError( + f'Unit "{unit}" greater than {UNIT_MAX_LEN} characters.' + ) return unit @@ -61,17 +88,57 @@ def ensure_value_type( This does not guarantee that the value will be the correct type for the given parameter, only that any data coming in is in the format that we expect. For now, the only transformation it is doing is converting integers represented - as floating points to integers. If something is labelled as an int but is not actually an integer, that will be - caught when it is attempted to be set as the parameter value and will raise the appropriate error there. + as floating points to integers, and bools represented as 1.0/0.0 to True/False, and floating points represented as + ints to floats. + + If something is labelled as a type but does not get converted here, that will be caught when it is attempted to be + set as the parameter value and will raise the appropriate error there. """ - validated_value: AllowedTypes - if isinstance(value, float) and parameter_type is int and value.is_integer(): - validated_value = int(value) - else: - validated_value = value + validated_value: AllowedTypes = value + if isinstance(value, float): + if parameter_type is bool and (value == 0 or value == 1): + validated_value = bool(value) + elif parameter_type is int and value.is_integer(): + validated_value = int(value) + elif ( + isinstance(value, int) + and not isinstance(value, bool) + and parameter_type is float + ): + validated_value = float(value) return validated_value +def ensure_float_value(value: Union[float, int]) -> float: + """Ensures that if we are expecting a float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_optional_float_value(value: Optional[Union[float, int]]) -> Optional[float]: + """Ensures that if we are expecting an optional float and receive an int, that will be converted to a float.""" + if not isinstance(value, bool) and isinstance(value, int): + return float(value) + return value + + +def ensure_float_choices( + choices: Optional[List[ParameterChoice]], +) -> Optional[List[ParameterChoice]]: + """Ensures that if we are expecting float parameter choices and any are int types, those will be converted.""" + if choices is not None: + return [ + ParameterChoice( + display_name=choice["display_name"], + # Type ignore because if for some reason this is a str or bool, that will raise in `validate_options` + value=ensure_float_value(choice["value"]), # type: ignore[arg-type] + ) + for choice in choices + ] + return choices + + def convert_type_string_for_enum( parameter_type: type, ) -> Literal["int", "float", "str"]: @@ -84,7 +151,7 @@ def convert_type_string_for_enum( return "str" else: raise ParameterValueError( - f"Cannot resolve parameter type {parameter_type} for an enumerated parameter." + f"Cannot resolve parameter type '{parameter_type.__name__}' for an enumerated parameter." ) @@ -96,7 +163,7 @@ def convert_type_string_for_num_param(parameter_type: type) -> Literal["int", "f return "float" else: raise ParameterValueError( - f"Cannot resolve parameter type {parameter_type} for a number parameter." + f"Cannot resolve parameter type '{parameter_type.__name__}' for a number parameter." ) @@ -122,7 +189,7 @@ def _validate_choices( ensure_display_name(display_name) if not isinstance(value, parameter_type): raise ParameterDefinitionError( - f"All choices provided must match type {type(parameter_type)}" + f"All choices provided must be of type '{parameter_type.__name__}'" ) @@ -141,21 +208,27 @@ def _validate_min_and_max( "If a maximum value is provided a minimum must also be provided." ) elif maximum is not None and minimum is not None: - if isinstance(maximum, (int, float)) and isinstance(minimum, (int, float)): - if maximum <= minimum: + if parameter_type is int or parameter_type is float: + if not isinstance(minimum, parameter_type): raise ParameterDefinitionError( - "Maximum must be greater than the minimum" + f"Minimum is type '{type(minimum).__name__}'," + f" but must be of parameter type '{parameter_type.__name__}'" ) - - if not isinstance(minimum, parameter_type) or not isinstance( - maximum, parameter_type - ): + if not isinstance(maximum, parameter_type): + raise ParameterDefinitionError( + f"Maximum is type '{type(maximum).__name__}'," + f" but must be of parameter type '{parameter_type.__name__}'" + ) + # These asserts are for the type checker and should never actually be asserted false + assert isinstance(minimum, (int, float)) + assert isinstance(maximum, (int, float)) + if maximum < minimum: raise ParameterDefinitionError( - f"Minimum and maximum must match type {parameter_type}" + "Maximum must be greater than the minimum" ) else: raise ParameterDefinitionError( - "Only parameters of type float or int can have a minimum and maximum" + "Only parameters of type float or int can have a minimum and maximum." ) @@ -163,7 +236,8 @@ def validate_type(value: ParamType, parameter_type: type) -> None: """Validate parameter value is the correct type.""" if not isinstance(value, parameter_type): raise ParameterValueError( - f"Parameter value has type {type(value)} must match type {parameter_type}." + f"Parameter value {value} has type '{type(value).__name__}'," + f" but must be of type '{parameter_type.__name__}'." ) @@ -175,7 +249,11 @@ def validate_options( parameter_type: type, ) -> None: """Validate default values and all possible constraints for a valid parameter definition.""" - validate_type(default, parameter_type) + if not isinstance(default, parameter_type): + raise ParameterValueError( + f"Parameter default {default} has type '{type(default).__name__}'," + f" but must be of type '{parameter_type.__name__}'." + ) if choices is None and minimum is None and maximum is None: raise ParameterDefinitionError( diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index c5f48c9d1bd9..9626fa86b961 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -54,7 +54,7 @@ from opentrons.legacy_broker import LegacyBroker from opentrons.config import IS_ROBOT from opentrons import protocol_api -from opentrons.commands import types as command_types +from opentrons.legacy_commands import types as command_types from opentrons.protocols import parse, bundle from opentrons.protocols.types import ( @@ -114,7 +114,7 @@ # TODO(mm, 2023-10-05): Type _SimulateResultRunLog more precisely by using TypedDicts from -# opentrons.commands. +# opentrons.legacy_commands. _SimulateResultRunLog = List[Mapping[str, Any]] _SimulateResult = Tuple[_SimulateResultRunLog, Optional[BundleContents]] @@ -223,6 +223,7 @@ def get_protocol_api( # type checking, like Jupyter Notebook. *, robot_type: Optional[_UserSpecifiedRobotType] = None, + use_virtual_hardware: bool = True, ) -> protocol_api.ProtocolContext: """ Build and return a ``protocol_api.ProtocolContext`` @@ -260,6 +261,7 @@ def get_protocol_api( :param robot_type: The type of robot to simulate: either ``"Flex"`` or ``"OT-2"``. If you're running this function on a robot, the default is the type of that robot. Otherwise, the default is ``"OT-2"``, for backwards compatibility. + :param use_virtual_hardware: If true, use the protocol engines virtual hardware, if false use the lower level hardware simulator. :return: The protocol context. """ if isinstance(version, str): @@ -317,6 +319,7 @@ def get_protocol_api( hardware_api=checked_hardware, bundled_data=bundled_data, extra_labware=extra_labware, + use_virtual_hardware=use_virtual_hardware, ) # Intentional difference from execute.get_protocol_api(): @@ -453,7 +456,7 @@ def simulate( - ``payload``: The command. The human-readable run log text is available at ``payload["text"]``. The other keys of ``payload`` are command-dependent; - see ``opentrons.commands``. + see ``opentrons.legacy_commands``. .. note:: In older software versions, ``payload["text"]`` was a @@ -790,6 +793,7 @@ def _create_live_context_pe( deck_type: str, extra_labware: Dict[str, "LabwareDefinitionDict"], bundled_data: Optional[Dict[str, bytes]], + use_virtual_hardware: bool = True, ) -> ProtocolContext: """Return a live ProtocolContext that controls the robot through ProtocolEngine.""" assert api_version >= ENGINE_CORE_API_VERSION @@ -798,7 +802,9 @@ def _create_live_context_pe( pe, loop = _LIVE_PROTOCOL_ENGINE_CONTEXTS.enter_context( create_protocol_engine_in_thread( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config( + robot_type, virtual=use_virtual_hardware + ), drop_tips_after_run=False, post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE, load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol( @@ -899,7 +905,7 @@ def _run_file_pe( async def run(protocol_source: ProtocolSource) -> _SimulateResult: protocol_engine = await create_protocol_engine( hardware_api=hardware_api.wrapped(), - config=_get_protocol_engine_config(robot_type), + config=_get_protocol_engine_config(robot_type, virtual=True), load_fixed_trash=should_load_fixed_trash(protocol_source.config), ) @@ -934,15 +940,15 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult: return asyncio.run(run(protocol_source)) -def _get_protocol_engine_config(robot_type: RobotType) -> Config: +def _get_protocol_engine_config(robot_type: RobotType, virtual: bool) -> Config: """Return a Protocol Engine config to execute protocols on this device.""" return Config( robot_type=robot_type, deck_type=DeckType(deck_type_for_simulation(robot_type)), ignore_pause=True, - use_virtual_pipettes=True, - use_virtual_modules=True, - use_virtual_gripper=True, + use_virtual_pipettes=virtual, + use_virtual_modules=virtual, + use_virtual_gripper=virtual, use_simulated_deck_config=True, ) diff --git a/api/src/opentrons/system/camera.py b/api/src/opentrons/system/camera.py index 1c2d09d87476..761a9ba66a15 100644 --- a/api/src/opentrons/system/camera.py +++ b/api/src/opentrons/system/camera.py @@ -1,6 +1,7 @@ import asyncio import os from pathlib import Path + from opentrons.config import ARCHITECTURE, SystemArchitecture from opentrons_shared_data.errors.exceptions import CommunicationError from opentrons_shared_data.errors.codes import ErrorCodes @@ -29,7 +30,7 @@ async def take_picture(filename: Path) -> None: pass if ARCHITECTURE == SystemArchitecture.YOCTO: - cmd = f"v4l2-ctl --device /dev/video0 --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-to={str(filename)} --stream-count=1" + cmd = f"v4l2-ctl --device /dev/video2 --set-fmt-video=width=1280,height=720,pixelformat=MJPG --stream-mmap --stream-to={str(filename)} --stream-count=1" elif ARCHITECTURE == SystemArchitecture.BUILDROOT: cmd = f"ffmpeg -f video4linux2 -s 640x480 -i /dev/video0 -ss 0:0:1 -frames 1 {str(filename)}" else: # HOST diff --git a/api/src/opentrons/util/performance_helpers.py b/api/src/opentrons/util/performance_helpers.py new file mode 100644 index 000000000000..ddd547e2ce78 --- /dev/null +++ b/api/src/opentrons/util/performance_helpers.py @@ -0,0 +1,76 @@ +"""Performance helpers for tracking robot context.""" + +from pathlib import Path +from opentrons_shared_data.performance.dev_types import ( + SupportsTracking, + F, + RobotContextState, +) +from opentrons_shared_data.robot.dev_types import RobotTypeEnum +from typing import Callable, Type +from opentrons.config import ( + feature_flags as ff, + get_performance_metrics_data_dir, + robot_configs, +) + + +_should_track = ff.enable_performance_metrics( + RobotTypeEnum.robot_literal_to_enum(robot_configs.load().model) +) + + +def _handle_package_import() -> Type[SupportsTracking]: + """Handle the import of the performance_metrics package. + + If the package is not available, return a stubbed tracker. + """ + try: + from performance_metrics import RobotContextTracker + + return RobotContextTracker + except ImportError: + return StubbedTracker + + +package_to_use = _handle_package_import() +_robot_context_tracker: SupportsTracking | None = None + + +class StubbedTracker(SupportsTracking): + """A stubbed tracker that does nothing.""" + + def __init__(self, storage_location: Path, should_track: bool) -> None: + """Initialize the stubbed tracker.""" + pass + + def track(self, state: RobotContextState) -> Callable[[F], F]: + """Return the function unchanged.""" + + def inner_decorator(func: F) -> F: + """Return the function unchanged.""" + return func + + return inner_decorator + + def store(self) -> None: + """Do nothing.""" + pass + + +def _get_robot_context_tracker() -> SupportsTracking: + """Singleton for the robot context tracker.""" + global _robot_context_tracker + if _robot_context_tracker is None: + # TODO: replace with path lookup and should_store lookup + _robot_context_tracker = package_to_use( + get_performance_metrics_data_dir(), _should_track + ) + return _robot_context_tracker + + +def track_analysis(func: F) -> F: + """Track the analysis of a protocol.""" + return _get_robot_context_tracker().track(RobotContextState.ANALYZING_PROTOCOL)( + func + ) diff --git a/api/tests/opentrons/calibration_storage/test_deck_configuration.py b/api/tests/opentrons/calibration_storage/test_deck_configuration.py index 3cb8d59535fd..afdd4449eb42 100644 --- a/api/tests/opentrons/calibration_storage/test_deck_configuration.py +++ b/api/tests/opentrons/calibration_storage/test_deck_configuration.py @@ -10,8 +10,12 @@ def test_deck_configuration_serdes() -> None: """Test that deck configuration serialization/deserialization survives a round trip.""" dummy_cutout_fixture_placements = [ - CutoutFixturePlacement(cutout_fixture_id="a", cutout_id="b"), - CutoutFixturePlacement(cutout_fixture_id="c", cutout_id="d"), + CutoutFixturePlacement( + cutout_fixture_id="a", cutout_id="b", opentrons_module_serial_number="1" + ), + CutoutFixturePlacement( + cutout_fixture_id="c", cutout_id="d", opentrons_module_serial_number="2" + ), ] dummy_datetime = datetime(year=1961, month=5, day=6, tzinfo=timezone.utc) diff --git a/api/tests/opentrons/cli/test_cli.py b/api/tests/opentrons/cli/test_cli.py index eae5aa31ccc9..007a7dd6a030 100644 --- a/api/tests/opentrons/cli/test_cli.py +++ b/api/tests/opentrons/cli/test_cli.py @@ -1,4 +1,6 @@ """Test cli execution.""" + + import json import tempfile import textwrap @@ -9,8 +11,17 @@ import pytest from click.testing import CliRunner +from opentrons.util.performance_helpers import _get_robot_context_tracker + -from opentrons.cli.analyze import analyze +# Enable tracking for the RobotContextTracker +# This must come before the import of the analyze CLI +context_tracker = _get_robot_context_tracker() + +# Ignore the type error for the next line, as we're setting a private attribute for testing purposes +context_tracker._should_track = True # type: ignore[attr-defined] + +from opentrons.cli.analyze import analyze # noqa: E402 def _list_fixtures(version: int) -> Iterator[Path]: @@ -242,3 +253,24 @@ def test_python_error_line_numbers( assert result.json_output is not None [error] = result.json_output["errors"] assert error["detail"] == expected_detail + + +def test_track_analysis(tmp_path: Path) -> None: + """Test that the RobotContextTracker tracks analysis.""" + protocol_source = textwrap.dedent( + """ + requirements = {"apiLevel": "2.15"} + + def run(protocol): + pass + """ + ) + + protocol_source_file = tmp_path / "protocol.py" + protocol_source_file.write_text(protocol_source, encoding="utf-8") + + before_analysis = len(context_tracker._storage) # type: ignore[attr-defined] + + _get_analysis_result([protocol_source_file]) + + assert len(context_tracker._storage) == before_analysis + 1 # type: ignore[attr-defined] diff --git a/api/tests/opentrons/config/test_advanced_settings.py b/api/tests/opentrons/config/test_advanced_settings.py index b81b9149c676..17122fca0ddb 100644 --- a/api/tests/opentrons/config/test_advanced_settings.py +++ b/api/tests/opentrons/config/test_advanced_settings.py @@ -34,6 +34,15 @@ def mock_settings_values_flex() -> Dict[str, Optional[bool]]: } +@pytest.fixture +def mock_settings_values_flex_all() -> Dict[str, Optional[bool]]: + return { + s.id: False + for s in advanced_settings.settings + if RobotTypeEnum.FLEX in s.robot_type + } + + @pytest.fixture def mock_settings_values_empty() -> Dict[str, Optional[bool]]: return {s.id: None for s in advanced_settings.settings} @@ -57,12 +66,12 @@ def mock_settings( @pytest.fixture def mock_read_settings_file_ot2( - mock_settings_values_ot2: Dict[str, Optional[bool]], + mock_settings_values_ot2_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_ot2, + settings_map=mock_settings_values_ot2_all, version=mock_settings_version, ) yield p @@ -70,12 +79,12 @@ def mock_read_settings_file_ot2( @pytest.fixture def mock_read_settings_file_flex( - mock_settings_values_flex: Dict[str, Optional[bool]], + mock_settings_values_flex_all: Dict[str, Optional[bool]], mock_settings_version: int, ) -> Generator[MagicMock, None, None]: with patch("opentrons.config.advanced_settings._read_settings_file") as p: p.return_value = advanced_settings.SettingsData( - settings_map=mock_settings_values_flex, + settings_map=mock_settings_values_flex_all, version=mock_settings_version, ) yield p @@ -168,19 +177,19 @@ def test_get_all_adv_settings_empty( async def test_set_adv_setting( mock_read_settings_file_ot2: MagicMock, - mock_settings_values_ot2: MagicMock, + mock_settings_values_ot2_all: MagicMock, mock_write_settings_file: MagicMock, mock_settings_version: int, restore_restart_required: None, ) -> None: - for k, v in mock_settings_values_ot2.items(): + for k, v in mock_settings_values_ot2_all.items(): # Toggle the advanced setting await advanced_settings.set_adv_setting(k, not v) mock_write_settings_file.assert_called_with( # Only the current key is toggled { nk: nv if nk != k else not v - for nk, nv in mock_settings_values_ot2.items() + for nk, nv in mock_settings_values_ot2_all.items() }, mock_settings_version, CONFIG["feature_flags_file"], diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index e1c3f51b6515..e3269433db5b 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -8,7 +8,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 32 + return 33 # make sure to set a boolean value in default_file_settings only if @@ -31,6 +31,7 @@ def default_file_settings() -> Dict[str, Any]: "estopNotRequired": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } @@ -392,6 +393,18 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: return r +@pytest.fixture +def v33_config(v32_config: Dict[str, Any]) -> Dict[str, Any]: + r = v32_config.copy() + r.update( + { + "_version": 33, + "enablePerformanceMetrics": None, + } + ) + return r + + @pytest.fixture( scope="session", params=[ @@ -429,6 +442,7 @@ def v32_config(v31_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v30_config"), lazy_fixture("v31_config"), lazy_fixture("v32_config"), + lazy_fixture("v33_config"), ], ) def old_settings(request: SubRequest) -> Dict[str, Any]: @@ -522,4 +536,5 @@ def test_ensures_config() -> None: "disableOverpressureDetection": None, "enableErrorRecoveryExperiments": None, "enableOEMMode": None, + "enablePerformanceMetrics": None, } diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index dcf6b6c4e370..de731268bce8 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -40,7 +40,7 @@ from opentrons_shared_data.deck.dev_types import ( RobotModel, DeckDefinitionV3, - DeckDefinitionV4, + DeckDefinitionV5, ) from opentrons_shared_data.deck import ( load as load_deck, @@ -256,7 +256,7 @@ def deck_definition_name(robot_model: RobotModel) -> str: @pytest.fixture -def deck_definition(deck_definition_name: str) -> DeckDefinitionV4: +def deck_definition(deck_definition_name: str) -> DeckDefinitionV5: return load_deck(deck_definition_name, DEFAULT_DECK_DEFINITION_VERSION) diff --git a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py index 6aa3ca2a009a..d1f705d596f3 100644 --- a/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py +++ b/api/tests/opentrons/hardware_control/instruments/test_instrument_calibration.py @@ -134,9 +134,9 @@ def test_load_tip_length( (top_types.Point(0, 1.0, 1.5), top_types.Point(-1, 0, 0.2), True), # If both points are non-zero but at least one element is more than # the range different the test should fail - (top_types.Point(0.1, -1, 1.5), top_types.Point(1.7, 0, 0.2), False), - (top_types.Point(0.1, -1, 1.5), top_types.Point(0.6, 0.6, 1.3), False), - (top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 5), False), + (top_types.Point(0.1, -1, 4.3), top_types.Point(1.7, 0, 0.2), False), + (top_types.Point(0.1, -3.2, 1.5), top_types.Point(0.6, 0.9, 1.3), False), + (top_types.Point(0.1, -1, 1.5), top_types.Point(-0.2, -0.1, 6), False), ], ) def test_instrument_consistency_check_ot3( @@ -151,4 +151,4 @@ def test_instrument_consistency_check_ot3( top_types.Mount.LEFT: left, top_types.Mount.RIGHT: right, } - assert result[0].limit == 1.5 + assert result[0].limit == 4.0 diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index ce92ad2c1a86..eb3d0e48c6c8 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -3,6 +3,7 @@ from pathlib import Path from unittest import mock +from packaging.version import Version from opentrons.hardware_control import ExecutionManager from opentrons.hardware_control.modules import ModuleAtPort @@ -22,6 +23,7 @@ HeaterShaker, AbstractModule, ) +from opentrons.hardware_control.modules.mod_abc import parse_fw_version from opentrons.drivers.rpi_drivers.types import USBPort @@ -422,3 +424,20 @@ def test_magnetic_module_revision_parsing(revision, model): ) def test_temperature_module_revision_parsing(revision, model): assert TempDeck._model_from_revision(revision) == model + + +@pytest.mark.parametrize( + argnames=["device_version", "expected_result"], + argvalues=[ + ["v1.0.4", Version("v1.0.4")], + ["v0.5.6", Version("v0.5.6")], + ["v1.0.4-dhfs", Version("v0.0.0")], + ["v3.0.dshjfd", Version("v0.0.0")], + ], +) +async def test_catch_invalid_fw_version( + device_version: str, + expected_result: bool, +) -> None: + """Assert that invalid firmware versions prompt a valid Version object of v0.0.0.""" + assert parse_fw_version(device_version) == expected_result diff --git a/api/src/opentrons/commands/__init__.py b/api/tests/opentrons/legacy_commands/__init__.py similarity index 100% rename from api/src/opentrons/commands/__init__.py rename to api/tests/opentrons/legacy_commands/__init__.py diff --git a/api/tests/opentrons/commands/test_protocol_commands.py b/api/tests/opentrons/legacy_commands/test_protocol_commands.py similarity index 96% rename from api/tests/opentrons/commands/test_protocol_commands.py rename to api/tests/opentrons/legacy_commands/test_protocol_commands.py index e7fb31aed1cc..1ff5475f95b1 100644 --- a/api/tests/opentrons/commands/test_protocol_commands.py +++ b/api/tests/opentrons/legacy_commands/test_protocol_commands.py @@ -1,5 +1,5 @@ import pytest -from opentrons.commands import protocol_commands +from opentrons.legacy_commands import protocol_commands @pytest.mark.parametrize( diff --git a/api/tests/opentrons/commands/test_publisher.py b/api/tests/opentrons/legacy_commands/test_publisher.py similarity index 97% rename from api/tests/opentrons/commands/test_publisher.py rename to api/tests/opentrons/legacy_commands/test_publisher.py index a88e6c045231..359b6b3c5fde 100644 --- a/api/tests/opentrons/commands/test_publisher.py +++ b/api/tests/opentrons/legacy_commands/test_publisher.py @@ -1,12 +1,16 @@ -"""Tests for opentrons.commands.publisher.""" +"""Tests for opentrons.legacy_commands.publisher.""" from __future__ import annotations import pytest from decoy import Decoy, matchers from typing import Any, Dict, cast from opentrons.legacy_broker import LegacyBroker -from opentrons.commands.types import Command as CommandDict, CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish, publish_context +from opentrons.legacy_commands.types import Command as CommandDict, CommandMessage +from opentrons.legacy_commands.publisher import ( + CommandPublisher, + publish, + publish_context, +) @pytest.fixture diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3b296067a0d1..6ac0e9aaaf0d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -276,7 +276,7 @@ def test_pick_up_tip( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), - mock_engine_client.pick_up_tip( + mock_engine_client.pick_up_tip_wait_for_recovery( pipette_id="abc123", labware_id="labware-id", well_name="well-name", diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index fdf12f1e51b5..8f6589b11043 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -7,7 +7,10 @@ from decoy import Decoy from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV5, + SlotDefV3, +) from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons_shared_data.labware.dev_types import ( LabwareDefinition as LabwareDefDict, @@ -85,15 +88,15 @@ @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.fixture(autouse=True) @@ -180,7 +183,7 @@ def test_api_version( def test_get_slot_definition( - ot2_standard_deck_def: DeckDefinitionV4, subject: ProtocolCore, decoy: Decoy + ot2_standard_deck_def: DeckDefinitionV5, subject: ProtocolCore, decoy: Decoy ) -> None: """It should return a deck slot's definition.""" expected_slot_def = cast( @@ -1154,7 +1157,7 @@ def test_add_labware_definition( EngineModuleModel.THERMOCYCLER_MODULE_V2, ThermocyclerModuleCore, lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A1, + DeckSlotName.SLOT_B1, "OT-3 Standard", ), ( @@ -1177,7 +1180,7 @@ def test_load_module( engine_model: EngineModuleModel, expected_core_cls: Type[ModuleCore], subject: ProtocolCore, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, slot_name: DeckSlotName, robot_type: RobotType, ) -> None: @@ -1193,12 +1196,22 @@ def test_load_module( [mock_hw_mod_1, mock_hw_mod_2] ) - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast( - SlotDefV3, - {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + if robot_type == "OT-2 Standard": + decoy.when(subject.get_slot_definition(slot_name)).then_return( + cast( + SlotDefV3, + {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + ) ) - ) + else: + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + slot_name + ) + ).then_return("cutout" + slot_name.value) decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) @@ -1251,97 +1264,6 @@ def test_load_module( assert subject.get_labware_on_module(result) is None -@pytest.mark.parametrize( - ( - "requested_model", - "engine_model", - "expected_core_cls", - "deck_def", - "slot_name", - "robot_type", - ), - [ - ( - TemperatureModuleModel.TEMPERATURE_V2, - EngineModuleModel.TEMPERATURE_MODULE_V2, - TemperatureModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_D2, - "OT-3 Standard", - ), - ( - MagneticModuleModel.MAGNETIC_V2, - EngineModuleModel.MAGNETIC_MODULE_V2, - MagneticModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ( - ThermocyclerModuleModel.THERMOCYCLER_V1, - EngineModuleModel.THERMOCYCLER_MODULE_V1, - ThermocyclerModuleCore, - lazy_fixture("ot2_standard_deck_def"), - DeckSlotName.SLOT_1, - "OT-2 Standard", - ), - ( - ThermocyclerModuleModel.THERMOCYCLER_V2, - EngineModuleModel.THERMOCYCLER_MODULE_V2, - ThermocyclerModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ( - HeaterShakerModuleModel.HEATER_SHAKER_V1, - EngineModuleModel.HEATER_SHAKER_MODULE_V1, - HeaterShakerModuleCore, - lazy_fixture("ot3_standard_deck_def"), - DeckSlotName.SLOT_A2, - "OT-3 Standard", - ), - ], -) -def test_load_module_raises_wrong_location( - decoy: Decoy, - mock_engine_client: EngineClient, - mock_sync_hardware_api: SyncHardwareAPI, - requested_model: ModuleModel, - engine_model: EngineModuleModel, - expected_core_cls: Type[ModuleCore], - subject: ProtocolCore, - deck_def: DeckDefinitionV4, - slot_name: DeckSlotName, - robot_type: RobotType, -) -> None: - """It should issue a load module engine command.""" - mock_hw_mod_1 = decoy.mock(cls=AbstractModule) - mock_hw_mod_2 = decoy.mock(cls=AbstractModule) - - decoy.when(mock_hw_mod_1.device_info).then_return({"serial": "abc123"}) - decoy.when(mock_hw_mod_2.device_info).then_return({"serial": "xyz789"}) - decoy.when(mock_sync_hardware_api.attached_modules).then_return( - [mock_hw_mod_1, mock_hw_mod_2] - ) - - decoy.when(mock_engine_client.state.config.robot_type).then_return(robot_type) - - decoy.when(subject.get_slot_definition(slot_name)).then_return( - cast(SlotDefV3, {"compatibleModuleTypes": []}) - ) - - with pytest.raises( - ValueError, - match=f"A {ModuleType.from_model(requested_model).value} cannot be loaded into slot {slot_name}", - ): - subject.load_module( - model=requested_model, - deck_slot=slot_name, - configuration=None, - ) - - # APIv2.15 because we're expecting a fixed trash. @pytest.mark.parametrize("api_version", [APIVersion(2, 15)]) def test_load_mag_block( @@ -1349,7 +1271,7 @@ def test_load_mag_block( mock_engine_client: EngineClient, mock_sync_hardware_api: SyncHardwareAPI, subject: ProtocolCore, - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should issue a load module engine command.""" definition = ModuleDefinition.construct() # type: ignore[call-arg] @@ -1366,6 +1288,14 @@ def test_load_mag_block( }, ) ) + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(ot3_standard_deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_A2 + ) + ).then_return("cutout" + DeckSlotName.SLOT_A2.value) decoy.when( mock_engine_client.load_module( @@ -1440,7 +1370,7 @@ def test_load_module_thermocycler_with_no_location( requested_model: ModuleModel, engine_model: EngineModuleModel, subject: ProtocolCore, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, expected_slot: DeckSlotName, ) -> None: """It should issue a load module engine command with location at 7.""" @@ -1450,12 +1380,14 @@ def test_load_module_thermocycler_with_no_location( decoy.when(mock_hw_mod.device_info).then_return({"serial": "xyz789"}) decoy.when(mock_sync_hardware_api.attached_modules).then_return([mock_hw_mod]) decoy.when(mock_engine_client.state.config.robot_type).then_return("OT-3 Standard") - decoy.when(subject.get_slot_definition(expected_slot)).then_return( - cast( - SlotDefV3, - {"compatibleModuleTypes": [ModuleType.from_model(requested_model)]}, + decoy.when( + mock_engine_client.state.addressable_areas.state.deck_definition + ).then_return(deck_def) + decoy.when( + mock_engine_client.state.addressable_areas.get_cutout_id_by_deck_slot_name( + expected_slot ) - ) + ).then_return("cutout" + expected_slot.value) decoy.when( mock_engine_client.load_module( @@ -1590,7 +1522,7 @@ def test_get_deck_definition( decoy: Decoy, mock_engine_client: EngineClient, subject: ProtocolCore ) -> None: """It should return the loaded deck definition from engine state.""" - deck_definition = cast(DeckDefinitionV4, {"schemaVersion": "4"}) + deck_definition = cast(DeckDefinitionV5, {"schemaVersion": "5"}) decoy.when(mock_engine_client.state.labware.get_deck_definition()).then_return( deck_definition diff --git a/api/tests/opentrons/protocol_api/test_deck.py b/api/tests/opentrons/protocol_api/test_deck.py index b3dc4716449a..f471cb936e15 100644 --- a/api/tests/opentrons/protocol_api/test_deck.py +++ b/api/tests/opentrons/protocol_api/test_deck.py @@ -5,7 +5,7 @@ import pytest from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5, SlotDefV3 from opentrons.motion_planning import adjacent_slots_getters as mock_adjacent_slots from opentrons.protocols.api_support.types import APIVersion @@ -23,10 +23,10 @@ @pytest.fixture -def deck_definition() -> DeckDefinitionV4: +def deck_definition() -> DeckDefinitionV5: """Get a deck definition value object.""" return cast( - DeckDefinitionV4, + DeckDefinitionV5, { "locations": {"addressableAreas": [], "calibrationPoints": []}, "cutoutFixtures": {}, @@ -81,7 +81,7 @@ def staging_slot_definitions_by_name() -> Dict[str, SlotDefV3]: @pytest.fixture def subject( decoy: Decoy, - deck_definition: DeckDefinitionV4, + deck_definition: DeckDefinitionV5, mock_protocol_core: ProtocolCore, mock_core_map: LoadedCoreMap, api_version: APIVersion, diff --git a/api/tests/opentrons/protocol_api/test_module_context.py b/api/tests/opentrons/protocol_api/test_module_context.py index 6ce8928abc4b..c57f1ff52dc5 100644 --- a/api/tests/opentrons/protocol_api/test_module_context.py +++ b/api/tests/opentrons/protocol_api/test_module_context.py @@ -108,7 +108,7 @@ def test_load_labware( decoy.when(mock_labware_core.get_well_columns()).then_return([]) result = subject.load_labware( - name="infinite tip rack", + name="Infinite Tip Rack", label="it doesn't run out", namespace="ideal", version=101, diff --git a/api/tests/opentrons/protocol_api/test_parameter_context.py b/api/tests/opentrons/protocol_api/test_parameter_context.py index 8b98ae204cac..7dcc246f216d 100644 --- a/api/tests/opentrons/protocol_api/test_parameter_context.py +++ b/api/tests/opentrons/protocol_api/test_parameter_context.py @@ -46,6 +46,9 @@ def subject(api_version: APIVersion) -> ParameterContext: def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add an int parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cool variable") decoy.when( @@ -60,6 +63,7 @@ def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: unit="foot candles", ) ).then_return(param_def) + subject.add_int( display_name="abc", variable_name="xyz", @@ -70,25 +74,37 @@ def test_add_int(decoy: Decoy, subject: ParameterContext) -> None: description="blah blah blah", unit="foot candles", ) + assert param_def is subject._parameters["my cool variable"] + decoy.verify(mock_validation.validate_variable_name_unique("xyz", {"other_param"})) def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a float parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my cooler variable") + decoy.when(mock_validation.ensure_float_value(12.3)).then_return(3.21) + decoy.when(mock_validation.ensure_optional_float_value(4.5)).then_return(5.4) + decoy.when(mock_validation.ensure_optional_float_value(67.8)).then_return(87.6) + decoy.when( + mock_validation.ensure_float_choices([{"display_name": "foo", "value": 4.2}]) + ).then_return([{"display_name": "bar", "value": 2.4}]) decoy.when( mock_parameter_definition.create_float_parameter( display_name="abc", variable_name="xyz", - default=12.3, - minimum=4.5, - maximum=67.8, - choices=[{"display_name": "foo", "value": 4.2}], + default=3.21, + minimum=5.4, + maximum=87.6, + choices=[{"display_name": "bar", "value": 2.4}], description="blah blah blah", unit="lux", ) ).then_return(param_def) + subject.add_float( display_name="abc", variable_name="xyz", @@ -99,11 +115,16 @@ def test_add_float(decoy: Decoy, subject: ParameterContext) -> None: description="blah blah blah", unit="lux", ) + assert param_def is subject._parameters["my cooler variable"] + decoy.verify(mock_validation.validate_variable_name_unique("xyz", {"other_param"})) def test_add_bool(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a boolean parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my coolest variable") decoy.when( @@ -118,17 +139,23 @@ def test_add_bool(decoy: Decoy, subject: ParameterContext) -> None: description="lorem ipsum", ) ).then_return(param_def) + subject.add_bool( display_name="cba", variable_name="zxy", default=False, description="lorem ipsum", ) + assert param_def is subject._parameters["my coolest variable"] + decoy.verify(mock_validation.validate_variable_name_unique("zxy", {"other_param"})) def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: """It should create and add a string parameter definition.""" + subject._parameters["other_param"] = decoy.mock( + cls=mock_parameter_definition.ParameterDefinition + ) param_def = decoy.mock(cls=mock_parameter_definition.ParameterDefinition) decoy.when(param_def.variable_name).then_return("my slightly less cool variable") decoy.when( @@ -140,6 +167,7 @@ def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: description="fee foo fum", ) ).then_return(param_def) + subject.add_str( display_name="jkl", variable_name="qwerty", @@ -147,7 +175,11 @@ def test_add_string(decoy: Decoy, subject: ParameterContext) -> None: choices=[{"display_name": "bar", "value": "aaa"}], description="fee foo fum", ) + assert param_def is subject._parameters["my slightly less cool variable"] + decoy.verify( + mock_validation.validate_variable_name_unique("qwerty", {"other_param"}) + ) def test_set_parameters(decoy: Decoy, subject: ParameterContext) -> None: @@ -193,5 +225,5 @@ def test_export_parameters_for_protocol( subject._parameters = {"foo": param_def_1, "bar": param_def_2} result = subject.export_parameters_for_protocol() - assert result.x == "a" # type: ignore [attr-defined] - assert result.y == 1.23 # type: ignore [attr-defined] + assert result.x == "a" # type: ignore[attr-defined] + assert result.y == 1.23 # type: ignore[attr-defined] diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_module.py b/api/tests/opentrons/protocol_engine/commands/test_load_module.py index 84be22d4661a..65306f34adcf 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_module.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_module.py @@ -1,9 +1,11 @@ """Test load module command.""" import pytest +from typing import cast from decoy import Decoy from opentrons.protocol_engine.errors import LocationIsOccupiedError from opentrons.protocol_engine.state import StateView +from opentrons_shared_data.robot.dev_types import RobotType from opentrons.types import DeckSlotName from opentrons.protocol_engine.types import ( DeckSlotLocation, @@ -11,12 +13,30 @@ ModuleDefinition, ) from opentrons.protocol_engine.execution import EquipmentHandler, LoadedModuleData +from opentrons.protocol_engine import ModuleModel as EngineModuleModel +from opentrons.hardware_control.modules import ModuleType from opentrons.protocol_engine.commands.load_module import ( LoadModuleParams, LoadModuleResult, LoadModuleImplementation, ) +from opentrons.hardware_control.modules.types import ( + ModuleModel as HardwareModuleModel, + TemperatureModuleModel, + MagneticModuleModel, + ThermocyclerModuleModel, + HeaterShakerModuleModel, +) +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV5, + SlotDefV3, +) +from opentrons_shared_data.deck import load as load_deck +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) async def test_load_module_implementation( @@ -29,19 +49,29 @@ async def test_load_module_implementation( subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) data = LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) decoy.when( await equipment.load_module( - model=ModuleModel.TEMPERATURE_MODULE_V1, + model=ModuleModel.TEMPERATURE_MODULE_V2, location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), module_id="some-id", ) @@ -73,12 +103,22 @@ async def test_load_module_implementation_mag_block( data = LoadModuleParams( model=ModuleModel.MAGNETIC_BLOCK_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_return(DeckSlotLocation(slotName=DeckSlotName.SLOT_2)) @@ -114,16 +154,162 @@ async def test_load_module_raises_if_location_occupied( subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) data = LoadModuleParams( - model=ModuleModel.TEMPERATURE_MODULE_V1, - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + model=ModuleModel.TEMPERATURE_MODULE_V2, + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D1), moduleId="some-id", ) + deck_def = load_deck(STANDARD_OT3_DECK, 5) + + decoy.when(state_view.addressable_areas.state.deck_definition).then_return(deck_def) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name( + DeckSlotName.SLOT_D1 + ) + ).then_return("cutout" + DeckSlotName.SLOT_D1.value) + decoy.when( state_view.geometry.ensure_location_not_occupied( - DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + DeckSlotLocation(slotName=DeckSlotName.SLOT_D1) ) ).then_raise(LocationIsOccupiedError("Get your own spot!")) with pytest.raises(LocationIsOccupiedError): await subject.execute(data) + + +@pytest.mark.parametrize( + ( + "requested_model", + "engine_model", + "deck_def", + "slot_name", + "robot_type", + ), + [ + ( + TemperatureModuleModel.TEMPERATURE_V2, + EngineModuleModel.TEMPERATURE_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_D2, + "OT-3 Standard", + ), + ( + ThermocyclerModuleModel.THERMOCYCLER_V1, + EngineModuleModel.THERMOCYCLER_MODULE_V1, + load_deck(STANDARD_OT2_DECK, 5), + DeckSlotName.SLOT_1, + "OT-2 Standard", + ), + ( + ThermocyclerModuleModel.THERMOCYCLER_V2, + EngineModuleModel.THERMOCYCLER_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ( + HeaterShakerModuleModel.HEATER_SHAKER_V1, + EngineModuleModel.HEATER_SHAKER_MODULE_V1, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ], +) +async def test_load_module_raises_wrong_location( + decoy: Decoy, + equipment: EquipmentHandler, + state_view: StateView, + requested_model: HardwareModuleModel, + engine_model: EngineModuleModel, + deck_def: DeckDefinitionV5, + slot_name: DeckSlotName, + robot_type: RobotType, +) -> None: + """It should issue a load module engine command.""" + subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) + + data = LoadModuleParams( + model=engine_model, + location=DeckSlotLocation(slotName=slot_name), + moduleId="some-id", + ) + + decoy.when(state_view.config.robot_type).then_return(robot_type) + + if robot_type == "OT-2 Standard": + decoy.when( + state_view.addressable_areas.get_slot_definition(slot_name.id) + ).then_return(cast(SlotDefV3, {"compatibleModuleTypes": []})) + else: + decoy.when(state_view.addressable_areas.state.deck_definition).then_return( + deck_def + ) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot_name) + ).then_return("cutout" + slot_name.value) + + with pytest.raises( + ValueError, + match=f"A {ModuleType.from_model(model=requested_model).value} cannot be loaded into slot {slot_name}", + ): + await subject.execute(data) + + +@pytest.mark.parametrize( + ( + "requested_model", + "engine_model", + "deck_def", + "slot_name", + "robot_type", + ), + [ + ( + MagneticModuleModel.MAGNETIC_V2, + EngineModuleModel.MAGNETIC_MODULE_V2, + load_deck(STANDARD_OT3_DECK, 5), + DeckSlotName.SLOT_A2, + "OT-3 Standard", + ), + ], +) +async def test_load_module_raises_module_fixture_id_does_not_exist( + decoy: Decoy, + equipment: EquipmentHandler, + state_view: StateView, + requested_model: HardwareModuleModel, + engine_model: EngineModuleModel, + deck_def: DeckDefinitionV5, + slot_name: DeckSlotName, + robot_type: RobotType, +) -> None: + """It should issue a load module engine command and raise an error for unmatched fixtures.""" + subject = LoadModuleImplementation(equipment=equipment, state_view=state_view) + + data = LoadModuleParams( + model=engine_model, + location=DeckSlotLocation(slotName=slot_name), + moduleId="some-id", + ) + + decoy.when(state_view.config.robot_type).then_return(robot_type) + + if robot_type == "OT-2 Standard": + decoy.when( + state_view.addressable_areas.get_slot_definition(slot_name.id) + ).then_return(cast(SlotDefV3, {"compatibleModuleTypes": []})) + else: + decoy.when(state_view.addressable_areas.state.deck_definition).then_return( + deck_def + ) + decoy.when( + state_view.addressable_areas.get_cutout_id_by_deck_slot_name(slot_name) + ).then_return("cutout" + slot_name.value) + + with pytest.raises( + ValueError, + match=f"Module Type {ModuleType.from_model(requested_model).value} does not have a related fixture ID.", + ): + await subject.execute(data) diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index dfd59089c2d1..ab23f7e9e088 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -7,7 +7,7 @@ from opentrons_shared_data import load_shared_data from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.labware import load_definition from opentrons_shared_data.pipette import pipette_definition from opentrons.protocols.models import LabwareDefinition @@ -57,21 +57,21 @@ def ot3_hardware_api(decoy: Decoy) -> OT3API: @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot2_short_trash_deck_def() -> DeckDefinitionV4: +def ot2_short_trash_deck_def() -> DeckDefinitionV5: """Get the OT-2 short-trash deck definition.""" - return load_deck(SHORT_TRASH_DECK, 4) + return load_deck(SHORT_TRASH_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.fixture(scope="session") diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 94b7ad255090..1cdb051164c5 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -360,8 +360,8 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: False, ), ( - EStopActivatedError("oh no"), - matchers.ErrorMatching(PE_EStopActivatedError, match="oh no"), + EStopActivatedError(), + matchers.ErrorMatching(PE_EStopActivatedError), True, ), ( @@ -500,6 +500,7 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: action_dispatcher.dispatch( FailCommandAction( command_id="command-id", + running_command=running_command, error_id="error-id", failed_at=datetime(year=2023, month=3, day=3), error=expected_error, diff --git a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py index 69a249ebfc27..1177894e977d 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_equipment_handler.py @@ -22,7 +22,6 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import errors -from opentrons.protocol_engine.actions import ActionDispatcher from opentrons.protocol_engine.types import ( DeckSlotLocation, DeckType, @@ -84,12 +83,6 @@ def state_store(decoy: Decoy) -> StateStore: return decoy.mock(cls=StateStore) -@pytest.fixture -def action_dispatcher(decoy: Decoy) -> ActionDispatcher: - """Get a mocked out ActionDispatcher instance.""" - return decoy.mock(cls=ActionDispatcher) - - @pytest.fixture def model_utils(decoy: Decoy) -> ModelUtils: """Get a mocked out ModelUtils instance.""" @@ -166,7 +159,6 @@ def virtual_pipette_data_provider( def subject( hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, labware_data_provider: LabwareDataProvider, module_data_provider: ModuleDataProvider, model_utils: ModelUtils, @@ -176,7 +168,6 @@ def subject( return EquipmentHandler( hardware_api=hardware_api, state_store=state_store, - action_dispatcher=action_dispatcher, labware_data_provider=labware_data_provider, module_data_provider=module_data_provider, model_utils=model_utils, @@ -614,7 +605,6 @@ async def test_load_pipette( model_utils: ModelUtils, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -665,7 +655,6 @@ async def test_load_pipette_96_channels( model_utils: ModelUtils, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -702,7 +691,6 @@ async def test_load_pipette_uses_provided_id( decoy: Decoy, hardware_api: HardwareControlAPI, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, ) -> None: @@ -734,7 +722,6 @@ async def test_load_pipette_use_virtual( decoy: Decoy, model_utils: ModelUtils, state_store: StateStore, - action_dispatcher: ActionDispatcher, loaded_static_pipette_data: LoadedStaticPipetteData, subject: EquipmentHandler, virtual_pipette_data_provider: pipette_data_provider.VirtualPipetteDataProvider, @@ -837,6 +824,7 @@ async def test_load_module( HardwareModule(serial_number="serial-1", definition=tempdeck_v1_def), HardwareModule(serial_number="serial-2", definition=tempdeck_v2_def), ], + expected_serial_number=None, ) ).then_return(HardwareModule(serial_number="serial-1", definition=tempdeck_v1_def)) diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py index 8071cc98a667..12b324955be9 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -5,7 +5,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.types import DeckSlotName @@ -13,7 +13,6 @@ FixtureDoesNotExistError, CutoutDoesNotExistError, AddressableAreaDoesNotExistError, - FixtureDoesNotProvideAreasError, ) from opentrons.protocol_engine.types import ( AddressableArea, @@ -33,21 +32,21 @@ @pytest.fixture(scope="session") -def ot2_standard_deck_def() -> DeckDefinitionV4: +def ot2_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT2_DECK, 4) + return load_deck(STANDARD_OT2_DECK, 5) @pytest.fixture(scope="session") -def ot2_short_trash_deck_def() -> DeckDefinitionV4: +def ot2_short_trash_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(SHORT_TRASH_DECK, 4) + return load_deck(SHORT_TRASH_DECK, 5) @pytest.fixture(scope="session") -def ot3_standard_deck_def() -> DeckDefinitionV4: +def ot3_standard_deck_def() -> DeckDefinitionV5: """Get the OT-2 standard deck definition.""" - return load_deck(STANDARD_OT3_DECK, 4) + return load_deck(STANDARD_OT3_DECK, 5) @pytest.mark.parametrize( @@ -73,7 +72,7 @@ def ot3_standard_deck_def() -> DeckDefinitionV4: def test_get_cutout_position( cutout_id: str, expected_deck_point: DeckPoint, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the deck position for the requested cutout id.""" cutout_position = subject.get_cutout_position(cutout_id, deck_def) @@ -81,7 +80,7 @@ def test_get_cutout_position( def test_get_cutout_position_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no cutout with that ID in the deck definition.""" with pytest.raises(CutoutDoesNotExistError): @@ -107,7 +106,7 @@ def test_get_cutout_position_raises( def test_get_cutout_fixture( cutout_fixture_id: str, expected_display_name: str, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the cutout fixture given the cutout fixture id.""" cutout_fixture = subject.get_cutout_fixture(cutout_fixture_id, deck_def) @@ -115,7 +114,7 @@ def test_get_cutout_fixture( def test_get_cutout_fixture_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if the given cutout fixture id does not exist.""" with pytest.raises(FixtureDoesNotExistError): @@ -149,7 +148,7 @@ def test_get_provided_addressable_area_names( cutout_fixture_id: str, cutout_id: str, expected_areas: List[str], - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the provided addressable area for the cutout fixture and cutout.""" provided_addressable_areas = subject.get_provided_addressable_area_names( @@ -158,16 +157,6 @@ def test_get_provided_addressable_area_names( assert provided_addressable_areas == expected_areas -def test_get_provided_addressable_area_raises( - ot3_standard_deck_def: DeckDefinitionV4, -) -> None: - """It should raise if the cutout fixture does not provide areas for the given cutout id.""" - with pytest.raises(FixtureDoesNotProvideAreasError): - subject.get_provided_addressable_area_names( - "singleRightSlot", "theFunCutout", ot3_standard_deck_def - ) - - @pytest.mark.parametrize( ( "addressable_area_name", @@ -223,7 +212,7 @@ def test_get_potential_cutout_fixtures( addressable_area_name: str, expected_cutout_id: str, expected_potential_fixtures: Set[PotentialCutoutFixture], - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get a cutout id and a set of potential cutout fixtures for an addressable area name.""" cutout_id, potential_fixtures = subject.get_potential_cutout_fixtures( @@ -234,7 +223,7 @@ def test_get_potential_cutout_fixtures( def test_get_potential_cutout_fixtures_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no fixtures that provide the requested area.""" with pytest.raises(AddressableAreaDoesNotExistError): @@ -288,11 +277,7 @@ def test_get_potential_cutout_fixtures_raises( display_name="Slot D1", bounding_box=Dimensions(x=128.0, y=86.0, z=0), position=AddressableOffsetVector(x=1, y=2, z=3), - compatible_module_types=[ - "temperatureModuleType", - "heaterShakerModuleType", - "magneticBlockType", - ], + compatible_module_types=[], ), lazy_fixture("ot3_standard_deck_def"), ), @@ -327,7 +312,7 @@ def test_get_potential_cutout_fixtures_raises( def test_get_addressable_area_from_name( addressable_area_name: str, expected_addressable_area: AddressableArea, - deck_def: DeckDefinitionV4, + deck_def: DeckDefinitionV5, ) -> None: """It should get the deck position for the requested cutout id.""" addressable_area = subject.get_addressable_area_from_name( @@ -337,7 +322,7 @@ def test_get_addressable_area_from_name( def test_get_addressable_area_from_name_raises( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> None: """It should raise if there is no addressable area by that name in the deck.""" with pytest.raises(AddressableAreaDoesNotExistError): diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py index f587d7ce5ddf..bd720777ed64 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_data_provider.py @@ -3,7 +3,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -31,7 +31,7 @@ def mock_labware_data_provider(decoy: Decoy) -> LabwareDataProvider: ) async def test_get_deck_definition( deck_type: DeckType, - expected_definition: DeckDefinitionV4, + expected_definition: DeckDefinitionV5, mock_labware_data_provider: LabwareDataProvider, ) -> None: """It should be able to load the correct deck definition.""" @@ -44,7 +44,7 @@ async def test_get_deck_definition( async def test_get_deck_labware_fixtures_ot2_standard( decoy: Decoy, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ot2_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: @@ -74,7 +74,7 @@ async def test_get_deck_labware_fixtures_ot2_standard( async def test_get_deck_labware_fixtures_ot2_short_trash( decoy: Decoy, - ot2_short_trash_deck_def: DeckDefinitionV4, + ot2_short_trash_deck_def: DeckDefinitionV5, ot2_short_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: @@ -104,7 +104,7 @@ async def test_get_deck_labware_fixtures_ot2_short_trash( async def test_get_deck_labware_fixtures_ot3_standard( decoy: Decoy, - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ot3_fixed_trash_def: LabwareDefinition, mock_labware_data_provider: LabwareDataProvider, ) -> None: diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 63e9cea29252..8a79d31ce929 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -1,7 +1,7 @@ """Addressable area state store tests.""" import pytest -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.labware.labware_definition import Parameters from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -35,24 +35,24 @@ def _make_deck_config() -> DeckConfigurationType: return [ - ("cutoutA1", "singleLeftSlot"), - ("cutoutB1", "singleLeftSlot"), - ("cutoutC1", "singleLeftSlot"), - ("cutoutD1", "singleLeftSlot"), - ("cutoutA2", "singleCenterSlot"), - ("cutoutB2", "singleCenterSlot"), - ("cutoutC2", "singleCenterSlot"), - ("cutoutD2", "singleCenterSlot"), - ("cutoutA3", "trashBinAdapter"), - ("cutoutB3", "singleRightSlot"), - ("cutoutC3", "stagingAreaRightSlot"), - ("cutoutD3", "wasteChuteRightAdapterNoCover"), + ("cutoutA1", "singleLeftSlot", None), + ("cutoutB1", "singleLeftSlot", None), + ("cutoutC1", "singleLeftSlot", None), + ("cutoutD1", "singleLeftSlot", None), + ("cutoutA2", "singleCenterSlot", None), + ("cutoutB2", "singleCenterSlot", None), + ("cutoutC2", "singleCenterSlot", None), + ("cutoutD2", "singleCenterSlot", None), + ("cutoutA3", "trashBinAdapter", None), + ("cutoutB3", "singleRightSlot", None), + ("cutoutC3", "stagingAreaRightSlot", None), + ("cutoutD3", "wasteChuteRightAdapterNoCover", None), ] @pytest.fixture def simulated_subject( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> AddressableAreaStore: """Get an AddressableAreaStore test subject, under simulated deck conditions.""" return AddressableAreaStore( @@ -68,7 +68,7 @@ def simulated_subject( @pytest.fixture def subject( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, ) -> AddressableAreaStore: """Get an AddressableAreaStore test subject.""" return AddressableAreaStore( @@ -83,7 +83,7 @@ def subject( def test_initial_state_simulated( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, simulated_subject: AddressableAreaStore, ) -> None: """It should create the Addressable Area store with no loaded addressable areas.""" @@ -98,7 +98,7 @@ def test_initial_state_simulated( def test_initial_state( - ot3_standard_deck_def: DeckDefinitionV4, + ot3_standard_deck_def: DeckDefinitionV5, subject: AddressableAreaStore, ) -> None: """It should create the Addressable Area store with loaded addressable areas.""" diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py index 34ddcaa37faa..e903c59a45da 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -6,7 +6,7 @@ from typing import Dict, Set, Optional, cast from opentrons_shared_data.robot.dev_types import RobotType -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.types import Point, DeckSlotName from opentrons.protocol_engine.errors import ( @@ -47,7 +47,7 @@ def get_addressable_area_view( potential_cutout_fixtures_by_cutout_id: Optional[ Dict[str, Set[PotentialCutoutFixture]] ] = None, - deck_definition: Optional[DeckDefinitionV4] = None, + deck_definition: Optional[DeckDefinitionV5] = None, deck_configuration: Optional[DeckConfigurationType] = None, robot_type: RobotType = "OT-3 Standard", use_simulated_deck_config: bool = False, @@ -57,7 +57,7 @@ def get_addressable_area_view( loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id or {}, - deck_definition=deck_definition or cast(DeckDefinitionV4, {"otId": "fake"}), + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), deck_configuration=deck_configuration or [], robot_type=robot_type, use_simulated_deck_config=use_simulated_deck_config, @@ -79,8 +79,8 @@ def test_get_all_cutout_fixtures_non_simulated_deck_config() -> None: """It should return the cutout fixtures from the deck config, if it's not simulated.""" subject = get_addressable_area_view( deck_configuration=[ - ("cutout-id-1", "cutout-fixture-id-1"), - ("cutout-id-2", "cutout-fixture-id-2"), + ("cutout-id-1", "cutout-fixture-id-1", None), + ("cutout-id-2", "cutout-fixture-id-2", None), ], use_simulated_deck_config=False, ) @@ -309,6 +309,8 @@ def test_get_fixture_height(decoy: Decoy) -> None: "height": 10, # These values don't matter: "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, "mayMountTo": [], "displayName": "", "providesAddressableAreas": {}, @@ -324,6 +326,8 @@ def test_get_fixture_height(decoy: Decoy) -> None: "height": 9000.1, # These values don't matter: "id": "id", + "expectOpentronsModuleSerialNumber": False, + "fixtureGroup": {}, "mayMountTo": [], "displayName": "", "providesAddressableAreas": {}, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_state.py b/api/tests/opentrons/protocol_engine/state/test_command_state.py new file mode 100644 index 000000000000..742abf3e6e91 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_command_state.py @@ -0,0 +1,471 @@ +"""Tests for the CommandStore+CommandState+CommandView trifecta. + +The trifecta is tested here as a single unit, treating CommandState as a private +implementation detail. +""" + +from datetime import datetime +from unittest.mock import sentinel + +import pytest + +from opentrons_shared_data.errors import ErrorCodes, PythonException + +from opentrons.ordered_set import OrderedSet +from opentrons.protocol_engine import actions, commands, errors +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError +from opentrons.protocol_engine.notes.notes import CommandNote +from opentrons.protocol_engine.state.commands import ( + CommandStore, + CommandView, +) +from opentrons.protocol_engine.state.config import Config +from opentrons.protocol_engine.types import DeckType, EngineStatus + + +def _make_config() -> Config: + return Config( + # Choice of robot and deck type is arbitrary. + robot_type="OT-2 Standard", + deck_type=DeckType.OT2_STANDARD, + ) + + +@pytest.mark.parametrize("error_recovery_type", ErrorRecoveryType) +def test_command_failure(error_recovery_type: ErrorRecoveryType) -> None: + """It should store an error and mark the command if it fails.""" + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + command_id = "command-id" + command_key = "command-key" + created_at = datetime(year=2021, month=1, day=1) + started_at = datetime(year=2022, month=2, day=2) + failed_at = datetime(year=2023, month=3, day=3) + error_id = "error-id" + notes = [ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ] + + params = commands.CommentParams(message="No comment.") + + subject.handle_action( + actions.QueueCommandAction( + command_id=command_id, + created_at=created_at, + request=commands.CommentCreate(params=params, key=command_key), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id=command_id, started_at=started_at) + ) + subject.handle_action( + actions.FailCommandAction( + command_id=command_id, + running_command=subject_view.get(command_id), + error_id=error_id, + failed_at=failed_at, + error=errors.ProtocolEngineError(message="oh no"), + notes=notes, + type=error_recovery_type, + ) + ) + + expected_error_occurrence = errors.ErrorOccurrence( + id=error_id, + errorType="ProtocolEngineError", + createdAt=failed_at, + detail="oh no", + errorCode=ErrorCodes.GENERAL_ERROR.value.code, + ) + expected_failed_command = commands.Comment( + id=command_id, + key=command_key, + commandType="comment", + createdAt=created_at, + startedAt=started_at, + completedAt=failed_at, + status=commands.CommandStatus.FAILED, + params=params, + result=None, + error=expected_error_occurrence, + notes=notes, + ) + + assert subject_view.get("command-id") == expected_failed_command + + +def test_command_failure_clears_queues() -> None: + """It should clear the command queue on command failure.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + assert subject_view.get_queue_ids() == OrderedSet() + assert subject_view.get_next_to_execute() is None + + +def test_setup_command_failure_only_clears_setup_command_queue() -> None: + """It should clear only the setup command queue for a failed setup command. + + This test queues up a non-setup command followed by two setup commands, + then runs and fails the first setup command. + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-2", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2_setup) + queue_3_setup = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), + intent=commands.CommandIntent.SETUP, + key="command-key-3", + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-3", + ) + subject.handle_action(queue_3_setup) + + run_2_setup = actions.RunCommandAction( + command_id="command-id-2", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_2_setup) + fail_2_setup = actions.FailCommandAction( + command_id="command-id-2", + running_command=subject_view.get("command-id-2"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.FAIL_RUN, + ) + subject.handle_action(fail_2_setup) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.QUEUED), + ("command-id-2", commands.CommandStatus.FAILED), + ("command-id-3", commands.CommandStatus.FAILED), + ] + assert subject_view.get_running_command_id() is None + + subject.handle_action( + actions.PlayAction(requested_at=datetime.now(), deck_configuration=None) + ) + assert subject_view.get_next_to_execute() == "command-id-1" + + +def test_nonfatal_command_failure() -> None: + """Test the command queue if a command fails recoverably. + + Commands that were after the failed command in the queue should be left in + the queue. + + The queue status should be "awaiting-recovery." + """ + subject = CommandStore(is_door_open=False, config=_make_config()) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-1" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-1", + ) + subject.handle_action(queue_1) + queue_2 = actions.QueueCommandAction( + request=commands.WaitForResumeCreate( + params=commands.WaitForResumeParams(), key="command-key-2" + ), + request_hash=None, + created_at=datetime(year=2021, month=1, day=1), + command_id="command-id-2", + ) + subject.handle_action(queue_2) + + run_1 = actions.RunCommandAction( + command_id="command-id-1", + started_at=datetime(year=2022, month=2, day=2), + ) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="command-id-1", + running_command=subject_view.get("command-id-1"), + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=errors.ProtocolEngineError(message="oh no"), + notes=[ + CommandNote( + noteKind="noteKind", + shortMessage="shortMessage", + longMessage="longMessage", + source="source", + ) + ], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + subject.handle_action(fail_1) + + assert [(c.id, c.status) for c in subject_view.get_all()] == [ + ("command-id-1", commands.CommandStatus.FAILED), + ("command-id-2", commands.CommandStatus.QUEUED), + ] + assert subject_view.get_running_command_id() is None + + +def test_error_recovery_type_tracking() -> None: + """It should keep track of each failed command's error recovery type.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + + subject.handle_action( + actions.QueueCommandAction( + command_id="c1", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.QueueCommandAction( + command_id="c2", + created_at=datetime.now(), + request=commands.CommentCreate( + params=commands.CommentParams(message="yeehaw"), + ), + request_hash=None, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + ) + running_command_1 = CommandView(subject.state).get("c1") + subject.handle_action( + actions.FailCommandAction( + command_id="c1", + running_command=running_command_1, + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + ) + ) + subject.handle_action( + actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + ) + running_command_2 = CommandView(subject.state).get("c2") + subject.handle_action( + actions.FailCommandAction( + command_id="c2", + running_command=running_command_2, + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError("new sheriff in town")), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + ) + ) + + view = CommandView(subject.state) + assert view.get_error_recovery_type("c1") == ErrorRecoveryType.WAIT_FOR_RECOVERY + assert view.get_error_recovery_type("c2") == ErrorRecoveryType.FAIL_RUN + + +def test_get_recovery_in_progress_for_command() -> None: + """It should return whether error recovery is in progress for the given command.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + queue_1 = actions.QueueCommandAction( + "c1", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_1) + run_1 = actions.RunCommandAction(command_id="c1", started_at=datetime.now()) + subject.handle_action(run_1) + fail_1 = actions.FailCommandAction( + command_id="c1", + error_id="c1-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c1"), + ) + subject.handle_action(fail_1) + + # c1 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_1_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_1_recovery) + + # c1 failed recoverably, but we've already completed its recovery. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + queue_2 = actions.QueueCommandAction( + "c2", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_2) + run_2 = actions.RunCommandAction(command_id="c2", started_at=datetime.now()) + subject.handle_action(run_2) + fail_2 = actions.FailCommandAction( + command_id="c2", + error_id="c2-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.WAIT_FOR_RECOVERY, + running_command=subject_view.get("c2"), + ) + subject.handle_action(fail_2) + + # c2 failed recoverably and we're currently recovering from it. + assert subject_view.get_recovery_in_progress_for_command("c2") + # ...and that means we're *not* currently recovering from c1, + # even though it failed recoverably before. + assert not subject_view.get_recovery_in_progress_for_command("c1") + + resume_from_2_recovery = actions.ResumeFromRecoveryAction() + subject.handle_action(resume_from_2_recovery) + queue_3 = actions.QueueCommandAction( + "c3", + created_at=datetime.now(), + request=commands.CommentCreate(params=commands.CommentParams(message="")), + request_hash=None, + ) + subject.handle_action(queue_3) + run_3 = actions.RunCommandAction(command_id="c3", started_at=datetime.now()) + subject.handle_action(run_3) + fail_3 = actions.FailCommandAction( + command_id="c3", + error_id="c3-error", + failed_at=datetime.now(), + error=PythonException(RuntimeError()), + notes=[], + type=ErrorRecoveryType.FAIL_RUN, + running_command=subject_view.get("c3"), + ) + subject.handle_action(fail_3) + + # c3 failed, but not recoverably. + assert not subject_view.get_recovery_in_progress_for_command("c2") + + +def test_final_state_after_estop() -> None: + """Test the final state of the run after it's E-stopped.""" + subject = CommandStore(config=_make_config(), is_door_open=False) + subject_view = CommandView(subject.state) + + error_details = actions.FinishErrorDetails( + error=EStopActivatedError(), error_id="error-id", created_at=datetime.now() + ) + expected_error_occurrence = ErrorOccurrence( + id=error_details.error_id, + createdAt=error_details.created_at, + errorCode=ErrorCodes.E_STOP_ACTIVATED.value.code, + errorType="EStopActivatedError", + detail="E-stop activated.", + ) + + subject.handle_action(actions.StopAction(from_estop=True)) + subject.handle_action(actions.FinishAction(error_details=error_details)) + subject.handle_action( + actions.HardwareStoppedAction( + completed_at=sentinel.hardware_stopped_action_completed_at, + finish_error_details=None, + ) + ) + + assert subject_view.get_status() == EngineStatus.FAILED + assert subject_view.get_error() == expected_error_occurrence diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py similarity index 70% rename from api/tests/opentrons/protocol_engine/state/test_command_store.py rename to api/tests/opentrons/protocol_engine/state/test_command_store_old.py index d5bfc1e963a8..52d5aa961ce5 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store_old.py @@ -1,4 +1,10 @@ -"""Tests for the command lifecycle state.""" +"""Tests for CommandStore. + +DEPRECATED: Testing CommandStore independently of CommandView is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from datetime import datetime from typing import NamedTuple, Type @@ -8,12 +14,10 @@ from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine.actions.actions import RunCommandAction -from opentrons.protocol_engine.notes.notes import CommandNote from opentrons.types import MountType, DeckSlotName from opentrons.hardware_control.types import DoorState from opentrons.protocol_engine import commands, errors -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.types import DeckSlotLocation, DeckType, WellLocation from opentrons.protocol_engine.state import Config from opentrons.protocol_engine.state.commands import ( @@ -27,7 +31,6 @@ from opentrons.protocol_engine.actions import ( QueueCommandAction, SucceedCommandAction, - FailCommandAction, PlayAction, PauseAction, PauseSource, @@ -79,6 +82,8 @@ def test_initial_state( run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -422,321 +427,6 @@ def test_running_command_id() -> None: assert subject.state.command_history.get_running_command() is None -def test_command_failure_clears_queues() -> None: - """It should clear the command queue on command failure.""" - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_failed_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_2 - ) - - -def test_setup_command_failure_only_clears_setup_command_queue() -> None: - """It should clear only the setup command queue for a failed setup command. - - This test queues up a non-setup command followed by two setup commands, - then attempts to run and fail the first setup command and - """ - cmd_1_non_setup = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - createdAt=datetime(year=2021, month=1, day=1), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - queue_action_1_non_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=cmd_1_non_setup.params, key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_action_2_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-2", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - queue_action_3_setup = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), - intent=commands.CommandIntent.SETUP, - key="command-key-3", - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-3", - ) - - run_action_cmd_2 = RunCommandAction( - command_id="command-id-2", - started_at=datetime(year=2022, month=2, day=2), - ) - failed_action_cmd_2 = FailCommandAction( - command_id="command-id-2", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - expected_failed_cmd_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorType="ProtocolEngineError", - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - expected_failed_cmd_3 = commands.WaitForResume( - id="command-id-3", - key="command-key-3", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - intent=commands.CommandIntent.SETUP, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_action_1_non_setup) - subject.handle_action(queue_action_2_setup) - subject.handle_action(queue_action_3_setup) - subject.handle_action(run_action_cmd_2) - subject.handle_action(failed_action_cmd_2) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-1"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - "command-id-3", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=cmd_1_non_setup - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_failed_cmd_2 - ) - assert subject.state.command_history.get("command-id-3") == CommandEntry( - index=2, command=expected_failed_cmd_3 - ) - - -def test_nonfatal_command_failure() -> None: - """Test the command queue if a command fails recoverably. - - Commands that were after the failed command in the queue should be left in - the queue. - """ - queue_1 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-1" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-1", - ) - queue_2 = QueueCommandAction( - request=commands.WaitForResumeCreate( - params=commands.WaitForResumeParams(), key="command-key-2" - ), - request_hash=None, - created_at=datetime(year=2021, month=1, day=1), - command_id="command-id-2", - ) - run_1 = RunCommandAction( - command_id="command-id-1", - started_at=datetime(year=2022, month=2, day=2), - ) - fail_1 = FailCommandAction( - command_id="command-id-1", - error_id="error-id", - failed_at=datetime(year=2023, month=3, day=3), - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.WAIT_FOR_RECOVERY, - ) - - expected_failed_1 = commands.WaitForResume( - id="command-id-1", - key="command-key-1", - error=errors.ErrorOccurrence( - id="error-id", - createdAt=datetime(year=2023, month=3, day=3), - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - errorType="ProtocolEngineError", - detail="oh no", - ), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=datetime(year=2023, month=3, day=3), - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.FAILED, - ) - expected_queued_2 = commands.WaitForResume( - id="command-id-2", - key="command-key-2", - error=None, - createdAt=datetime(year=2021, month=1, day=1), - startedAt=None, - completedAt=None, - params=commands.WaitForResumeParams(), - status=commands.CommandStatus.QUEUED, - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action(queue_1) - subject.handle_action(queue_2) - subject.handle_action(run_1) - subject.handle_action(fail_1) - - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_queue_ids() == OrderedSet(["command-id-2"]) - assert subject.state.command_history.get_all_ids() == [ - "command-id-1", - "command-id-2", - ] - assert subject.state.command_history.get("command-id-1") == CommandEntry( - index=0, command=expected_failed_1 - ) - assert subject.state.command_history.get("command-id-2") == CommandEntry( - index=1, command=expected_queued_2 - ) - - def test_command_store_keeps_commands_in_queue_order() -> None: """It should keep commands in the order they were originally enqueued.""" command_create_1_non_setup = commands.CommentCreate( @@ -826,6 +516,8 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -850,6 +542,8 @@ def test_command_store_handles_play_action(pause_source: PauseSource) -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -880,6 +574,8 @@ def test_command_store_handles_finish_action() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -904,8 +600,13 @@ def test_command_store_handles_finish_action_with_stopped() -> None: assert subject.state.run_result == RunResult.STOPPED -@pytest.mark.parametrize("from_estop", [True, False]) -def test_command_store_handles_stop_action(from_estop: bool) -> None: +@pytest.mark.parametrize( + ["from_estop", "expected_run_result"], + [(True, RunResult.FAILED), (False, RunResult.STOPPED)], +) +def test_command_store_handles_stop_action( + from_estop: bool, expected_run_result: RunResult +) -> None: """It should mark the engine as non-gracefully stopped on StopAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) @@ -919,12 +620,14 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: assert subject.state == CommandState( command_history=CommandHistory(), queue_status=QueueStatus.PAUSED, - run_result=RunResult.STOPPED, + run_result=expected_run_result, run_completed_at=None, is_door_blocking=False, run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=from_estop, @@ -954,6 +657,8 @@ def test_command_store_cannot_restart_after_should_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1085,6 +790,8 @@ def test_command_store_wraps_unknown_errors() -> None: ), run_started_at=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, latest_command_hash=None, stopped_by_estop=False, ) @@ -1145,6 +852,8 @@ def __init__(self, message: str) -> None: errorCode=ErrorCodes.PIPETTE_NOT_PRESENT.value.code, ), failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, @@ -1176,6 +885,8 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1207,6 +918,8 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=datetime(year=2021, month=1, day=1), latest_command_hash=None, stopped_by_estop=False, @@ -1217,99 +930,6 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() -def test_command_store_handles_command_failed() -> None: - """It should store an error and mark the command if it fails.""" - expected_error_occurrence = errors.ErrorOccurrence( - id="error-id", - errorType="ProtocolEngineError", - createdAt=datetime(year=2023, month=3, day=3), - detail="oh no", - errorCode=ErrorCodes.GENERAL_ERROR.value.code, - ) - - expected_failed_command = commands.Comment( - id="command-id", - commandType="comment", - key="command-key", - createdAt=datetime(year=2021, month=1, day=1), - startedAt=datetime(year=2022, month=2, day=2), - completedAt=expected_error_occurrence.createdAt, - status=commands.CommandStatus.FAILED, - params=commands.CommentParams(message="hello, world"), - result=None, - error=expected_error_occurrence, - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - ) - - subject = CommandStore(is_door_open=False, config=_make_config()) - - subject.handle_action( - QueueCommandAction( - command_id=expected_failed_command.id, - created_at=expected_failed_command.createdAt, - request=commands.CommentCreate( - params=expected_failed_command.params, key=expected_failed_command.key - ), - request_hash=None, - ) - ) - subject.handle_action( - RunCommandAction( - command_id=expected_failed_command.id, - # Ignore arg-type errors because we know this isn't None. - started_at=expected_failed_command.startedAt, # type: ignore[arg-type] - ) - ) - subject.handle_action( - FailCommandAction( - command_id=expected_failed_command.id, - error_id=expected_error_occurrence.id, - failed_at=expected_error_occurrence.createdAt, - error=errors.ProtocolEngineError(message="oh no"), - notes=[ - CommandNote( - noteKind="noteKind", - shortMessage="shortMessage", - longMessage="longMessage", - source="source", - ) - ], - type=ErrorRecoveryType.FAIL_RUN, - ) - ) - - failed_command_entry = CommandEntry(index=0, command=expected_failed_command) - command_history = CommandHistory() - command_history._add("command-id", failed_command_entry) - command_history._set_terminal_command_id("command-id") - - assert subject.state == CommandState( - command_history=command_history, - queue_status=QueueStatus.SETUP, - run_result=None, - run_completed_at=None, - is_door_blocking=False, - run_error=None, - finish_error=None, - failed_command=failed_command_entry, - run_started_at=None, - latest_command_hash=None, - stopped_by_estop=False, - ) - assert subject.state.command_history.get_running_command() is None - assert subject.state.command_history.get_all_ids() == ["command-id"] - assert subject.state.command_history.get_queue_ids() == OrderedSet() - assert subject.state.command_history.get_setup_queue_ids() == OrderedSet() - assert subject.state.command_history.get("command-id") == failed_command_entry - - def test_handles_hardware_stopped() -> None: """It should mark the hardware as stopped on HardwareStoppedAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) @@ -1327,6 +947,8 @@ def test_handles_hardware_stopped() -> None: run_error=None, finish_error=None, failed_command=None, + command_error_recovery_types={}, + recovery_target_command_id=None, run_started_at=None, latest_command_hash=None, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py similarity index 98% rename from api/tests/opentrons/protocol_engine/state/test_command_view.py rename to api/tests/opentrons/protocol_engine/state/test_command_view_old.py index 047230d4f6d5..a9b5fc92cc3c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view_old.py @@ -1,8 +1,14 @@ -"""Labware state store tests.""" +"""Tests for CommandView. + +DEPRECATED: Testing CommandView independently of CommandStore is no longer helpful. +Add new tests to test_command_state.py, where they can be tested together. +""" + + import pytest from contextlib import nullcontext as does_not_raise from datetime import datetime -from typing import List, NamedTuple, Optional, Sequence, Type, Union +from typing import Dict, List, NamedTuple, Optional, Sequence, Type, Union from opentrons.protocol_engine import EngineStatus, commands as cmd, errors from opentrons.protocol_engine.actions import ( @@ -14,6 +20,7 @@ ) from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction +from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.protocol_engine.state.commands import ( CommandState, CommandView, @@ -50,6 +57,8 @@ def get_command_view( queued_setup_command_ids: Sequence[str] = (), run_error: Optional[errors.ErrorOccurrence] = None, failed_command: Optional[CommandEntry] = None, + command_error_recovery_types: Optional[Dict[str, ErrorRecoveryType]] = None, + recovery_target_command_id: Optional[str] = None, finish_error: Optional[errors.ErrorOccurrence] = None, commands: Sequence[cmd.Command] = (), latest_command_hash: Optional[str] = None, @@ -81,6 +90,8 @@ def get_command_view( run_error=run_error, finish_error=finish_error, failed_command=failed_command, + command_error_recovery_types=command_error_recovery_types or {}, + recovery_target_command_id=recovery_target_command_id, run_started_at=run_started_at, latest_command_hash=latest_command_hash, stopped_by_estop=False, diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 731bcfb9a0ef..a390036bdcf9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -3,9 +3,10 @@ import pytest from decoy import Decoy -from typing import cast, List, Tuple, Optional, NamedTuple +from typing import cast, List, Tuple, Optional, NamedTuple, Dict, Set -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 +from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.labware.dev_types import LabwareUri from opentrons_shared_data.pipette import pipette_definition from opentrons.calibration_storage.helpers import uri_from_details @@ -26,6 +27,7 @@ ModuleLocation, OnLabwareLocation, AddressableAreaLocation, + AddressableArea, ModuleOffsetVector, ModuleOffsetData, LoadedLabware, @@ -45,6 +47,8 @@ LabwareMovementOffsetData, LoadedPipette, TipGeometry, + PotentialCutoutFixture, + DeckConfigurationType, ) from opentrons.protocol_engine.state import move_types from opentrons.protocol_engine.state.config import Config @@ -56,7 +60,10 @@ BoundingNozzlesOffsets, PipetteBoundingBoxOffsets, ) -from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType from ..pipette_fixtures import get_default_nozzle_map @@ -112,6 +119,30 @@ def subject( ) +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + def test_get_labware_parent_position( decoy: Decoy, labware_view: LabwareView, @@ -159,7 +190,7 @@ def test_get_labware_parent_position_on_module( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return a module position for labware on a module.""" @@ -178,10 +209,16 @@ def test_get_labware_parent_position_on_module( decoy.when( addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) ).then_return(Point(1, 2, 3)) + decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) + decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) + decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.THERMOCYCLER_MODULE_V2 ) @@ -207,7 +244,7 @@ def test_get_labware_parent_position_on_labware( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return a labware position for labware on a labware on a module.""" @@ -242,7 +279,10 @@ def test_get_labware_parent_position_on_labware( decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=1, y=2, z=3)) decoy.when(module_view.get_connected_model("module-id")).then_return( @@ -270,7 +310,7 @@ def test_module_calibration_offset_rotation( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """Return the rotated module calibration offset if the module was moved from one side of the deck to the other.""" @@ -395,7 +435,7 @@ def test_get_module_labware_highest_z( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the absolute location of a labware's highest Z point.""" @@ -422,7 +462,10 @@ def test_get_module_labware_highest_z( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_height_over_labware("module-id")).then_return(0.5) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( @@ -692,7 +735,7 @@ def test_get_highest_z_in_slot_with_single_module( module_view: ModuleView, addressable_area_view: AddressableAreaView, subject: GeometryView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> None: """It should get the highest Z in slot with just a single module.""" # Case: Slot has a module that doesn't have any labware on it. Highest z is equal to module height. @@ -707,9 +750,12 @@ def test_get_highest_z_in_slot_with_single_module( errors.LabwareNotLoadedOnModuleError("only module") ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) - decoy.when(module_view.get_module_highest_z(module_id="only-module")).then_return( - 12345 - ) + decoy.when( + module_view.get_module_highest_z( + module_id="only-module", + addressable_areas=addressable_area_view, + ) + ).then_return(12345) assert ( subject.get_highest_z_in_slot(DeckSlotLocation(slotName=DeckSlotName.SLOT_3)) @@ -820,7 +866,7 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( addressable_area_view: AddressableAreaView, subject: GeometryView, well_plate_def: LabwareDefinition, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> None: """It should get the highest z in slot of labware on module. @@ -883,7 +929,10 @@ def test_get_highest_z_in_slot_with_labware_stack_on_module( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=40, y=50, z=60)) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.TEMPERATURE_MODULE_V2 @@ -1056,7 +1105,7 @@ def test_get_module_labware_well_position( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware on module.""" @@ -1087,7 +1136,10 @@ def test_get_module_labware_well_position( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=4, y=5, z=6)) decoy.when(module_view.get_module_calibration_offset("module-id")).then_return( ModuleOffsetData( @@ -1544,7 +1596,7 @@ def test_get_labware_grip_point( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the grip point of the labware at the specified location.""" @@ -1567,7 +1619,7 @@ def test_get_labware_grip_point_on_labware( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should get the grip point of a labware on another labware.""" @@ -1614,7 +1666,7 @@ def test_get_labware_grip_point_for_labware_on_module( labware_view: LabwareView, module_view: ModuleView, addressable_area_view: AddressableAreaView, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: GeometryView, ) -> None: """It should return the grip point for labware directly on a module.""" @@ -1626,7 +1678,10 @@ def test_get_labware_grip_point_for_labware_on_module( ) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( - module_view.get_nominal_module_offset(module_id="module-id") + module_view.get_nominal_module_offset( + module_id="module-id", + addressable_areas=addressable_area_view, + ) ).then_return(LabwareOffsetVector(x=1, y=2, z=3)) decoy.when(module_view.get_connected_model("module-id")).then_return( ModuleModel.MAGNETIC_MODULE_V2 diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_store.py b/api/tests/opentrons/protocol_engine/state/test_labware_store.py index 2c0c8cdefd91..9d926583fb07 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_store.py @@ -4,7 +4,7 @@ from datetime import datetime from opentrons.calibration_storage.helpers import uri_from_details -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocols.models import LabwareDefinition from opentrons.types import DeckSlotName @@ -33,7 +33,7 @@ @pytest.fixture def subject( - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, ) -> LabwareStore: """Get a LabwareStore test subject.""" return LabwareStore( @@ -43,7 +43,7 @@ def subject( def test_initial_state( - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, subject: LabwareStore, ) -> None: """It should create the labware store with preloaded fixed labware.""" diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 5e7e96412fab..0f8086de6060 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -5,7 +5,7 @@ from contextlib import nullcontext as does_not_raise from opentrons_shared_data.deck import load as load_deck -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.pipette.dev_types import LabwareUri from opentrons_shared_data.labware import load_definition from opentrons_shared_data.labware.labware_definition import ( @@ -110,14 +110,14 @@ def get_labware_view( labware_by_id: Optional[Dict[str, LoadedLabware]] = None, labware_offsets_by_id: Optional[Dict[str, LabwareOffset]] = None, definitions_by_uri: Optional[Dict[str, LabwareDefinition]] = None, - deck_definition: Optional[DeckDefinitionV4] = None, + deck_definition: Optional[DeckDefinitionV5] = None, ) -> LabwareView: """Get a labware view test subject.""" state = LabwareState( labware_by_id=labware_by_id or {}, labware_offsets_by_id=labware_offsets_by_id or {}, definitions_by_uri=definitions_by_uri or {}, - deck_definition=deck_definition or cast(DeckDefinitionV4, {"fake": True}), + deck_definition=deck_definition or cast(DeckDefinitionV5, {"fake": True}), ) return LabwareView(state=state) @@ -696,7 +696,7 @@ def test_get_labware_overlap_offsets() -> None: class ModuleOverlapSpec(NamedTuple): """Spec data to test LabwareView.get_module_overlap_offsets.""" - spec_deck_definition: DeckDefinitionV4 + spec_deck_definition: DeckDefinitionV5 module_model: ModuleModel stacking_offset_with_module: Dict[str, SharedDataOverlapOffset] expected_offset: OverlapOffset @@ -705,7 +705,7 @@ class ModuleOverlapSpec(NamedTuple): module_overlap_specs: List[ModuleOverlapSpec] = [ ModuleOverlapSpec( # Labware on temp module on OT2, with stacking overlap for temp module - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.TEMPERATURE_MODULE_V2, stacking_offset_with_module={ str(ModuleModel.TEMPERATURE_MODULE_V2.value): SharedDataOverlapOffset( @@ -716,7 +716,7 @@ class ModuleOverlapSpec(NamedTuple): ), ModuleOverlapSpec( # Labware on TC Gen1 on OT2, with stacking overlap for TC Gen1 - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V1, stacking_offset_with_module={ str(ModuleModel.THERMOCYCLER_MODULE_V1.value): SharedDataOverlapOffset( @@ -727,21 +727,21 @@ class ModuleOverlapSpec(NamedTuple): ), ModuleOverlapSpec( # Labware on TC Gen2 on OT2, with no stacking overlap - spec_deck_definition=load_deck(STANDARD_OT2_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT2_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={}, expected_offset=OverlapOffset(x=0, y=0, z=10.7), ), ModuleOverlapSpec( # Labware on TC Gen2 on Flex, with no stacking overlap - spec_deck_definition=load_deck(STANDARD_OT3_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={}, expected_offset=OverlapOffset(x=0, y=0, z=0), ), ModuleOverlapSpec( # Labware on TC Gen2 on Flex, with stacking overlap for TC Gen2 - spec_deck_definition=load_deck(STANDARD_OT3_DECK, 4), + spec_deck_definition=load_deck(STANDARD_OT3_DECK, 5), module_model=ModuleModel.THERMOCYCLER_MODULE_V2, stacking_offset_with_module={ str(ModuleModel.THERMOCYCLER_MODULE_V2.value): SharedDataOverlapOffset( @@ -758,7 +758,7 @@ class ModuleOverlapSpec(NamedTuple): argvalues=module_overlap_specs, ) def test_get_module_overlap_offsets( - spec_deck_definition: DeckDefinitionV4, + spec_deck_definition: DeckDefinitionV5, module_model: ModuleModel, stacking_offset_with_module: Dict[str, SharedDataOverlapOffset], expected_offset: OverlapOffset, @@ -800,7 +800,7 @@ def test_get_default_magnet_height( assert subject.get_default_magnet_height(module_id="module-id", offset=2) == 12.0 -def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: +def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV5) -> None: """It should get the deck definition from the state.""" subject = get_labware_view(deck_definition=ot2_standard_deck_def) @@ -1404,7 +1404,7 @@ def test_raise_if_labware_cannot_be_stacked_on_labware_on_adapter() -> None: ) -def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV4) -> None: +def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV5) -> None: """It should get the deck's gripper offsets.""" subject = get_labware_view(deck_definition=ot3_standard_deck_def) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_store.py b/api/tests/opentrons/protocol_engine/state/test_module_store.py index 1d0d70034961..e6de0a96ac06 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_store.py @@ -1,8 +1,9 @@ """Module state store tests.""" -from typing import List +from typing import List, Set, cast, Dict, Optional import pytest from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from opentrons.types import DeckSlotName @@ -18,6 +19,9 @@ ModuleModel, HeaterShakerLatchStatus, DeckType, + AddressableArea, + DeckConfigurationType, + PotentialCutoutFixture, ) from opentrons.protocol_engine.state.modules import ( @@ -37,6 +41,11 @@ ThermocyclerModuleSubState, ModuleSubStateType, ) + +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.config import Config from opentrons.hardware_control.modules.types import LiveData @@ -48,9 +57,35 @@ ) +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + def test_initial_state() -> None: """It should initialize the module state.""" - subject = ModuleStore(config=_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) assert subject.state == ModuleState( deck_type=DeckType.OT2_STANDARD, @@ -158,7 +193,9 @@ def test_load_module( ), ) - subject = ModuleStore(config=_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action(action) assert subject.state == ModuleState( @@ -223,7 +260,7 @@ def test_load_thermocycler_in_thermocycler_slot( use_simulated_deck_config=False, robot_type=robot_type, deck_type=deck_type, - ) + ), ) subject.handle_action(action) @@ -302,7 +339,9 @@ def test_add_module_action( module_live_data=live_data, ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action(action) assert subject.state == ModuleState( @@ -343,7 +382,9 @@ def test_handle_hs_temperature_commands(heater_shaker_v1_def: ModuleDefinition) params=hs_commands.DeactivateHeaterParams(moduleId="module-id"), result=hs_commands.DeactivateHeaterResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -394,7 +435,9 @@ def test_handle_hs_shake_commands(heater_shaker_v1_def: ModuleDefinition) -> Non params=hs_commands.DeactivateShakerParams(moduleId="module-id"), result=hs_commands.DeactivateShakerResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -447,7 +490,9 @@ def test_handle_hs_labware_latch_commands( params=hs_commands.OpenLabwareLatchParams(moduleId="module-id"), result=hs_commands.OpenLabwareLatchResult(pipetteRetracted=False), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -511,7 +556,9 @@ def test_handle_tempdeck_temperature_commands( params=temp_commands.DeactivateTemperatureParams(moduleId="module-id"), result=temp_commands.DeactivateTemperatureResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -570,7 +617,9 @@ def test_handle_thermocycler_temperature_commands( params=tc_commands.DeactivateLidParams(moduleId="module-id"), result=tc_commands.DeactivateLidResult(), ) - subject = ModuleStore(_OT2_STANDARD_CONFIG) + subject = ModuleStore( + config=_OT2_STANDARD_CONFIG, + ) subject.handle_action( actions.SucceedCommandAction(private_result=None, command=load_module_cmd) @@ -652,7 +701,7 @@ def test_handle_thermocycler_lid_commands( use_simulated_deck_config=False, robot_type="OT-3 Standard", deck_type=DeckType.OT3_STANDARD, - ) + ), ) subject.handle_action( diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 77ab24bb336e..b840673f2e84 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -4,7 +4,21 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from contextlib import nullcontext as does_not_raise -from typing import ContextManager, Dict, NamedTuple, Optional, Type, Union, Any, List +from typing import ( + ContextManager, + Dict, + NamedTuple, + Optional, + Type, + Union, + Any, + List, + Set, + cast, +) + +from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data import load_shared_data from opentrons.types import DeckSlotName, MountType @@ -19,12 +33,19 @@ ModuleOffsetData, HeaterShakerLatchStatus, LabwareMovementOffsetData, + AddressableArea, + DeckConfigurationType, + PotentialCutoutFixture, ) from opentrons.protocol_engine.state.modules import ( ModuleView, ModuleState, HardwareModule, ) +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaView, + AddressableAreaState, +) from opentrons.protocol_engine.state.module_substates import ( HeaterShakerModuleSubState, @@ -37,6 +58,40 @@ ThermocyclerModuleId, ModuleSubStateType, ) +from opentrons_shared_data.deck import load as load_deck +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT3_DECK, +) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 5) + + +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV5] = None, + deck_configuration: Optional[DeckConfigurationType] = None, + robot_type: RobotType = "OT-3 Standard", + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV5, {"otId": "fake"}), + deck_configuration=deck_configuration or [], + robot_type=robot_type, + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) def make_module_view( @@ -332,41 +387,50 @@ def test_get_module_offset_for_ot2_standard( ) }, ) - assert subject.get_nominal_module_offset("module-id") == expected_offset + assert ( + subject.get_nominal_module_offset("module-id", get_addressable_area_view()) + == expected_offset + ) @pytest.mark.parametrize( - argnames=["module_def", "slot", "expected_offset"], + argnames=["module_def", "slot", "expected_offset", "deck_definition"], argvalues=[ ( lazy_fixture("tempdeck_v2_def"), DeckSlotName.SLOT_1.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=9), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("tempdeck_v2_def"), DeckSlotName.SLOT_3.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=9), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("thermocycler_v2_def"), DeckSlotName.SLOT_7.to_ot3_equivalent(), LabwareOffsetVector(x=-20.005, y=67.96, z=10.96), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("heater_shaker_v1_def"), DeckSlotName.SLOT_1.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=18.95), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("heater_shaker_v1_def"), DeckSlotName.SLOT_3.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=18.95), + lazy_fixture("ot3_standard_deck_def"), ), ( lazy_fixture("mag_block_v1_def"), - DeckSlotName.SLOT_2, + DeckSlotName.SLOT_2.to_ot3_equivalent(), LabwareOffsetVector(x=0, y=0, z=38.0), + lazy_fixture("ot3_standard_deck_def"), ), ], ) @@ -374,6 +438,7 @@ def test_get_module_offset_for_ot3_standard( module_def: ModuleDefinition, slot: DeckSlotName, expected_offset: LabwareOffsetVector, + deck_definition: DeckDefinitionV5, ) -> None: """It should return the correct labware offset for module in specified slot.""" subject = make_module_view( @@ -386,7 +451,16 @@ def test_get_module_offset_for_ot3_standard( ) }, ) - result_offset = subject.get_nominal_module_offset("module-id") + + result_offset = subject.get_nominal_module_offset( + "module-id", + get_addressable_area_view( + deck_configuration=None, + deck_definition=deck_definition, + use_simulated_deck_config=True, + ), + ) + assert (result_offset.x, result_offset.y, result_offset.z) == pytest.approx( (expected_offset.x, expected_offset.y, expected_offset.z) ) @@ -1767,10 +1841,20 @@ def test_get_default_gripper_offsets( @pytest.mark.parametrize( - argnames=["deck_type", "slot_name", "expected_highest_z"], + argnames=["deck_type", "slot_name", "expected_highest_z", "deck_definition"], argvalues=[ - (DeckType.OT2_STANDARD, DeckSlotName.SLOT_1, 84), - (DeckType.OT3_STANDARD, DeckSlotName.SLOT_D1, 12.91), + ( + DeckType.OT2_STANDARD, + DeckSlotName.SLOT_1, + 84, + lazy_fixture("ot3_standard_deck_def"), + ), + ( + DeckType.OT3_STANDARD, + DeckSlotName.SLOT_D1, + 12.91, + lazy_fixture("ot3_standard_deck_def"), + ), ], ) def test_get_module_highest_z( @@ -1778,6 +1862,7 @@ def test_get_module_highest_z( deck_type: DeckType, slot_name: DeckSlotName, expected_highest_z: float, + deck_definition: DeckDefinitionV5, ) -> None: """It should get the highest z point of the module.""" subject = make_module_view( @@ -1794,7 +1879,14 @@ def test_get_module_highest_z( }, ) assert isclose( - subject.get_module_highest_z(module_id="module-id"), + subject.get_module_highest_z( + module_id="module-id", + addressable_areas=get_addressable_area_view( + deck_configuration=None, + deck_definition=deck_definition, + use_simulated_deck_config=True, + ), + ), expected_highest_z, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index dd32bbec5911..515cbbd81e14 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -1,11 +1,11 @@ """Tests for the top-level StateStore/StateView.""" -from typing import Callable, Optional +from typing import Callable, Union from datetime import datetime import pytest from decoy import Decoy -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons.protocol_engine.actions import PlayAction from opentrons.protocol_engine.state import State, StateStore, Config @@ -32,7 +32,7 @@ def engine_config() -> Config: @pytest.fixture def subject( change_notifier: ChangeNotifier, - ot2_standard_deck_def: DeckDefinitionV4, + ot2_standard_deck_def: DeckDefinitionV5, engine_config: Config, ) -> StateStore: """Get a StateStore test subject.""" @@ -80,47 +80,52 @@ def test_notify_on_state_change( decoy.verify(change_notifier.notify(), times=1) -async def test_wait_for_state( +async def test_wait_for( decoy: Decoy, change_notifier: ChangeNotifier, subject: StateStore, ) -> None: """It should return an awaitable that signals state changes.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return( - None, - None, + 0, + 0, "hello world", ) - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" + decoy.verify(await change_notifier.wait(), times=2) + decoy.reset() + + decoy.when(check_condition("foo", bar="baz")).then_return( + "hello world", + "hello world again", + 0, + ) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 decoy.verify(await change_notifier.wait(), times=2) -async def test_wait_for_state_short_circuit( +async def test_wait_for_already_satisfied( decoy: Decoy, subject: StateStore, change_notifier: ChangeNotifier, ) -> None: - """It should short-circuit the change notifier if condition is satisfied.""" - check_condition: Callable[..., Optional[str]] = decoy.mock(name="check_condition") + """It should return immediately and skip the change notifier.""" + check_condition: Callable[..., Union[str, int]] = decoy.mock(name="check_condition") decoy.when(check_condition("foo", bar="baz")).then_return("hello world") - result = await subject.wait_for(check_condition, "foo", bar="baz") assert result == "hello world" - decoy.verify(await change_notifier.wait(), times=0) - -async def test_wait_for_already_true(decoy: Decoy, subject: StateStore) -> None: - """It should signal immediately if condition is already met.""" - check_condition = decoy.mock(name="check_condition") - decoy.when(check_condition()).then_return(True) - await subject.wait_for(check_condition) + decoy.when(check_condition("foo", bar="baz")).then_return(0) + result = await subject.wait_for_not(check_condition, "foo", bar="baz") + assert result == 0 + decoy.verify(await change_notifier.wait(), times=0) async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: @@ -131,3 +136,6 @@ async def test_wait_for_raises(decoy: Decoy, subject: StateStore) -> None: with pytest.raises(ValueError, match="oh no"): await subject.wait_for(check_condition) + + with pytest.raises(ValueError, match="oh no"): + await subject.wait_for_not(check_condition) diff --git a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py index b509946de756..2f7a0cae441a 100644 --- a/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_create_protocol_engine.py @@ -2,8 +2,9 @@ import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from opentrons_shared_data.robot.dev_types import RobotType +from opentrons_shared_data.deck import load as load_deck from opentrons.calibration_storage.helpers import uri_from_details from opentrons.hardware_control import API as HardwareAPI @@ -18,6 +19,30 @@ from opentrons.protocol_engine.types import DeckSlotLocation, LoadedLabware from opentrons.types import DeckSlotName +from opentrons.protocols.api_support.deck_type import ( + STANDARD_OT2_DECK, + SHORT_TRASH_DECK, + STANDARD_OT3_DECK, +) + + +@pytest.fixture(scope="session") +def ot2_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT2_DECK, 5) + + +@pytest.fixture(scope="session") +def ot2_short_trash_deck_def() -> DeckDefinitionV5: + """Get the OT-2 with short trash standard deck definition.""" + return load_deck(SHORT_TRASH_DECK, 5) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV5: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 5) + @pytest.mark.parametrize( ( @@ -47,7 +72,7 @@ async def test_create_engine_initializes_state_with_no_fixed_trash( hardware_api: HardwareAPI, robot_type: RobotType, deck_type: DeckType, - expected_deck_def: DeckDefinitionV4, + expected_deck_def: DeckDefinitionV5, ) -> None: """It should load deck geometry data into the store on create.""" engine = await create_protocol_engine( @@ -102,7 +127,7 @@ async def test_create_engine_initializes_state_with_fixed_trash( hardware_api: HardwareAPI, robot_type: RobotType, deck_type: DeckType, - expected_deck_def: DeckDefinitionV4, + expected_deck_def: DeckDefinitionV5, expected_fixed_trash_def: LabwareDefinition, expected_fixed_trash_slot: DeckSlotName, ) -> None: diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 2191b1c4954f..e3f7b315e4d9 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -2,14 +2,13 @@ import inspect from datetime import datetime from typing import Any +from unittest.mock import sentinel import pytest from decoy import Decoy from opentrons_shared_data.robot.dev_types import RobotType -from opentrons.ordered_set import OrderedSet from opentrons.protocol_engine.actions.actions import ResumeFromRecoveryAction -from opentrons.protocol_engine.error_recovery_policy import ErrorRecoveryType from opentrons.types import DeckSlotName from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI @@ -18,7 +17,6 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine import ProtocolEngine, commands, slot_standardization -from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import ( DeckType, LabwareOffset, @@ -58,7 +56,6 @@ QueueCommandAction, HardwareStoppedAction, ResetTipsAction, - FailCommandAction, ) @@ -333,6 +330,99 @@ def _stub_completed(*_a: object, **_k: object) -> bool: assert result == completed +async def test_add_and_execute_command_wait_for_recovery( + decoy: Decoy, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + model_utils: ModelUtils, + subject: ProtocolEngine, +) -> None: + """It should add and execute a command from a request.""" + created_at = datetime(year=2021, month=1, day=1) + original_request = commands.WaitForResumeCreate( + params=commands.WaitForResumeParams() + ) + standardized_request = commands.HomeCreate(params=commands.HomeParams()) + queued = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.QUEUED, + createdAt=created_at, + params=commands.HomeParams(), + ) + completed = commands.Home( + id="command-id", + key="command-key", + status=commands.CommandStatus.SUCCEEDED, + createdAt=created_at, + params=commands.HomeParams(), + ) + + robot_type: RobotType = "OT-3 Standard" + decoy.when(state_store.config).then_return( + Config(robot_type=robot_type, deck_type=DeckType.OT3_STANDARD) + ) + + decoy.when( + slot_standardization.standardize_command(original_request, robot_type) + ).then_return(standardized_request) + + decoy.when(model_utils.generate_id()).then_return("command-id") + decoy.when(model_utils.get_timestamp()).then_return(created_at) + + def _stub_queued(*_a: object, **_k: object) -> None: + decoy.when(state_store.commands.get("command-id")).then_return(queued) + + def _stub_completed(*_a: object, **_k: object) -> bool: + decoy.when(state_store.commands.get("command-id")).then_return(completed) + return True + + decoy.when( + state_store.commands.validate_action_allowed( + QueueCommandAction( + command_id="command-id", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_return( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + + decoy.when( + action_dispatcher.dispatch( + QueueCommandAction( + command_id="command-id-validated", + created_at=created_at, + request=standardized_request, + request_hash=None, + ) + ) + ).then_do(_stub_queued) + + decoy.when( + await state_store.wait_for( + condition=state_store.commands.get_command_is_final, + command_id="command-id", + ), + ).then_do(_stub_completed) + + result = await subject.add_and_execute_command_wait_for_recovery(original_request) + assert result == completed + decoy.verify( + await state_store.wait_for_not( + state_store.commands.get_recovery_in_progress_for_command, + "command-id", + ) + ) + + def test_play( decoy: Decoy, state_store: StateStore, @@ -421,7 +511,7 @@ def test_pause( state_store.commands.validate_action_allowed(expected_action), ).then_return(expected_action) - subject.pause() + subject.request_pause() decoy.verify( action_dispatcher.dispatch(expected_action), @@ -476,8 +566,8 @@ async def test_finish( """It should be able to gracefully tell the engine it's done.""" completed_at = datetime(2021, 1, 1, 0, 0) - decoy.when(model_utils.get_timestamp()).then_return(completed_at) decoy.when(state_store.commands.state.stopped_by_estop).then_return(False) + decoy.when(model_utils.get_timestamp()).then_return(completed_at) await subject.finish( drop_tips_after_run=drop_tips_after_run, @@ -716,7 +806,7 @@ async def test_stop( state_store.commands.validate_action_allowed(expected_action), ).then_return(expected_action) - await subject.stop() + await subject.request_stop() decoy.verify( action_dispatcher.dispatch(expected_action), @@ -742,7 +832,7 @@ async def test_stop_for_legacy_core_protocols( decoy.when(hardware_api.is_movement_execution_taskified()).then_return(True) - await subject.stop() + await subject.request_stop() decoy.verify( action_dispatcher.dispatch(expected_action), @@ -751,98 +841,53 @@ async def test_stop_for_legacy_core_protocols( ) -@pytest.mark.parametrize("maintenance_run", [True, False]) -async def test_estop_during_command( +async def test_estop( decoy: Decoy, action_dispatcher: ActionDispatcher, queue_worker: QueueWorker, state_store: StateStore, subject: ProtocolEngine, - model_utils: ModelUtils, - maintenance_run: bool, ) -> None: """It should be able to stop the engine.""" - timestamp = datetime(2021, 1, 1, 0, 0) - command_id = "command_fake_id" - error_id = "fake_error_id" - fake_command_set = OrderedSet(["fake-id-1", "fake-id-1"]) - - decoy.when(model_utils.get_timestamp()).then_return(timestamp) - decoy.when(model_utils.generate_id()).then_return(error_id) - decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) - decoy.when(state_store.commands.get_queue_ids()).then_return(fake_command_set) - - expected_action = FailCommandAction( - command_id=command_id, - error_id=error_id, - failed_at=timestamp, - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) - expected_action_2 = FailCommandAction( - command_id=fake_command_set.head(), - error_id=error_id, - failed_at=timestamp, - error=EStopActivatedError(message="Estop Activated"), - notes=[], - type=ErrorRecoveryType.FAIL_RUN, - ) + expected_action = StopAction(from_estop=True) + validated_action = sentinel.validated_action + decoy.when( + state_store.commands.validate_action_allowed(expected_action), + ).then_return(validated_action) - subject.estop(maintenance_run=maintenance_run) + subject.estop() decoy.verify( - action_dispatcher.dispatch(action=expected_action), - action_dispatcher.dispatch(action=expected_action_2), + action_dispatcher.dispatch(action=validated_action), queue_worker.cancel(), ) -@pytest.mark.parametrize("maintenance_run", [True, False]) -async def test_estop_without_command( +async def test_estop_noops_if_invalid( decoy: Decoy, action_dispatcher: ActionDispatcher, queue_worker: QueueWorker, state_store: StateStore, subject: ProtocolEngine, - model_utils: ModelUtils, - maintenance_run: bool, ) -> None: - """It should be able to stop the engine.""" - timestamp = datetime(2021, 1, 1, 0, 0) - error_id = "fake_error_id" - - decoy.when(model_utils.get_timestamp()).then_return(timestamp) - decoy.when(model_utils.generate_id()).then_return(error_id) - decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.get_running_command_id()).then_return(None) - decoy.when(state_store.commands.get_queue_ids()).then_return(OrderedSet()) - - expected_stop = StopAction(from_estop=True) - expected_hardware_stop = HardwareStoppedAction( - completed_at=timestamp, - finish_error_details=FinishErrorDetails( - error=EStopActivatedError(message="Estop Activated"), - error_id=error_id, - created_at=timestamp, - ), - ) - + """It should no-op if a stop is invalid right now..""" + expected_action = StopAction(from_estop=True) decoy.when( - state_store.commands.validate_action_allowed(expected_stop), - ).then_return(expected_stop) + state_store.commands.validate_action_allowed(expected_action), + ).then_raise(RuntimeError("unable to stop; this machine craves flesh")) - subject.estop(maintenance_run=maintenance_run) + subject.estop() # Should not raise. decoy.verify( - action_dispatcher.dispatch(expected_stop), times=1 if maintenance_run else 0 + action_dispatcher.dispatch(), # type: ignore + ignore_extra_args=True, + times=0, ) decoy.verify( - action_dispatcher.dispatch(expected_hardware_stop), - times=1 if maintenance_run else 0, + queue_worker.cancel(), + ignore_extra_args=True, + times=0, ) - decoy.verify(queue_worker.cancel(), times=1 if maintenance_run else 0) def test_add_plugin( diff --git a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py index 8a8ec50b7790..f04128788560 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_command_mapper.py @@ -7,7 +7,7 @@ from decoy import matchers, Decoy from opentrons.hardware_control.dev_types import PipetteDict -from opentrons.commands.types import CommentMessage, PauseMessage, CommandMessage +from opentrons.legacy_commands.types import CommentMessage, PauseMessage, CommandMessage from opentrons.protocol_engine import ( DeckSlotLocation, ModuleLocation, @@ -156,6 +156,7 @@ def test_map_after_with_error_command() -> None: assert result == [ pe_actions.FailCommandAction( command_id="command.COMMENT-0", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching( @@ -257,6 +258,7 @@ def test_command_stack() -> None: ), pe_actions.FailCommandAction( command_id="command.COMMENT-1", + running_command=matchers.Anything(), error_id=matchers.IsA(str), failed_at=matchers.IsA(datetime), error=matchers.ErrorMatching(LegacyContextCommandError, "oh no"), diff --git a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py index 1f7de8388ca8..f11676bcd377 100644 --- a/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py +++ b/api/tests/opentrons/protocol_runner/test_legacy_context_plugin.py @@ -5,7 +5,10 @@ from datetime import datetime from typing import Callable -from opentrons.commands.types import CommandMessage as LegacyCommand, PauseMessage +from opentrons.legacy_commands.types import ( + CommandMessage as LegacyCommand, + PauseMessage, +) from opentrons.protocol_engine import ( StateView, actions as pe_actions, diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 4f3ca342359d..68e215bf3dd4 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -1,4 +1,6 @@ """Tests for the PythonAndLegacyRunner, JsonRunner & LiveRunner classes.""" +from datetime import datetime + import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from decoy import Decoy, matchers @@ -18,7 +20,12 @@ from opentrons.util.broker import Broker from opentrons import protocol_reader -from opentrons.protocol_engine import ProtocolEngine, Liquid, commands as pe_commands +from opentrons.protocol_engine import ( + ProtocolEngine, + Liquid, + commands as pe_commands, + errors as pe_errors, +) from opentrons.protocol_reader import ( ProtocolSource, JsonProtocolConfig, @@ -231,7 +238,7 @@ def test_pause( """It should pause a protocol run with pause.""" subject.pause() - decoy.verify(protocol_engine.pause(), times=1) + decoy.verify(protocol_engine.request_pause(), times=1) @pytest.mark.parametrize( @@ -254,7 +261,7 @@ async def test_stop( subject.play() await subject.stop() - decoy.verify(await protocol_engine.stop(), times=1) + decoy.verify(await protocol_engine.request_stop(), times=1) @pytest.mark.parametrize( @@ -328,6 +335,96 @@ async def test_run_json_runner( ) +async def test_run_json_runner_stop_requested_stops_enquqing( + decoy: Decoy, + hardware_api: HardwareAPI, + protocol_engine: ProtocolEngine, + task_queue: TaskQueue, + json_runner_subject: JsonRunner, + json_file_reader: JsonFileReader, + json_translator: JsonTranslator, +) -> None: + """It should run a protocol to completion.""" + labware_definition = LabwareDefinition.construct() # type: ignore[call-arg] + json_protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/abc.json"), + files=[], + metadata={}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=6), + content_hash="abc123", + ) + + commands: List[pe_commands.CommandCreate] = [ + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + pe_commands.LoadLiquidCreate( + params=pe_commands.LoadLiquidParams( + liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} + ) + ), + ] + + liquids: List[Liquid] = [ + Liquid(id="water-id", displayName="water", description="water desc") + ] + + json_protocol = ProtocolSchemaV6.construct() # type: ignore[call-arg] + + decoy.when( + await protocol_reader.extract_labware_definitions(json_protocol_source) + ).then_return([labware_definition]) + decoy.when(json_file_reader.read(json_protocol_source)).then_return(json_protocol) + decoy.when(json_translator.translate_commands(json_protocol)).then_return(commands) + decoy.when(json_translator.translate_liquids(json_protocol)).then_return(liquids) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.HomeCreate(params=pe_commands.HomeParams()), + ) + ).then_return( + pe_commands.Home.construct(status=pe_commands.CommandStatus.SUCCEEDED) # type: ignore[call-arg] + ) + decoy.when( + await protocol_engine.add_and_execute_command( + pe_commands.WaitForDurationCreate( + params=pe_commands.WaitForDurationParams(seconds=10) + ), + ) + ).then_return( + pe_commands.WaitForDuration.construct( # type: ignore[call-arg] + error=pe_errors.ErrorOccurrence.from_failed( + id="some-id", + createdAt=datetime(year=2021, month=1, day=1), + error=pe_errors.ProtocolEngineError(), + ) + ) + ) + + await json_runner_subject.load(json_protocol_source) + + run_func_captor = matchers.Captor() + + decoy.verify( + protocol_engine.add_labware_definition(labware_definition), + protocol_engine.add_liquid( + id="water-id", name="water", description="water desc", color=None + ), + protocol_engine.add_command( + request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) + ), + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + + with pytest.raises(pe_errors.ProtocolEngineError): + await run_func() + + @pytest.mark.parametrize( "schema_version, json_protocol", [ @@ -385,6 +482,8 @@ async def test_load_json_runner( await json_runner_subject.load(json_protocol_source) + run_func_captor = matchers.Captor() + decoy.verify( protocol_engine.add_labware_definition(labware_definition), protocol_engine.add_liquid( @@ -393,24 +492,30 @@ async def test_load_json_runner( protocol_engine.add_command( request=pe_commands.HomeCreate(params=pe_commands.HomeParams(axes=None)) ), - protocol_engine.add_command( + task_queue.set_run_func(func=run_func_captor), + ) + + # Verify that the run func calls the right things: + run_func = run_func_captor.value + await run_func() + decoy.verify( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="hello") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.WaitForResumeCreate( params=pe_commands.WaitForResumeParams(message="goodbye") - ) + ), ), - protocol_engine.add_command( + await protocol_engine.add_and_execute_command( request=pe_commands.LoadLiquidCreate( params=pe_commands.LoadLiquidParams( liquidId="water-id", labwareId="labware-id", volumeByWell={"A1": 30} ) ), ), - task_queue.set_run_func(func=protocol_engine.wait_until_complete), ) diff --git a/api/tests/opentrons/protocols/duration/test_estimator.py b/api/tests/opentrons/protocols/duration/test_estimator.py index 926148696414..594f1cfad57c 100644 --- a/api/tests/opentrons/protocols/duration/test_estimator.py +++ b/api/tests/opentrons/protocols/duration/test_estimator.py @@ -3,7 +3,7 @@ import math import pytest -from opentrons.commands import types +from opentrons.legacy_commands import types from opentrons.protocol_api import InstrumentContext from opentrons.protocols.duration.estimator import ( DurationEstimator, diff --git a/api/tests/opentrons/protocols/parameters/test_validation.py b/api/tests/opentrons/protocols/parameters/test_validation.py index 988e203a8225..0ff337eb91da 100644 --- a/api/tests/opentrons/protocols/parameters/test_validation.py +++ b/api/tests/opentrons/protocols/parameters/test_validation.py @@ -12,6 +12,14 @@ from opentrons.protocols.parameters import validation as subject +def test_validate_variable_name_unique() -> None: + """It should no-op if the name is unique or if it's not a string, and raise if it is not.""" + subject.validate_variable_name_unique("one of a kind", {"fee", "foo", "fum"}) + subject.validate_variable_name_unique({}, {"fee", "foo", "fum"}) # type: ignore[arg-type] + with pytest.raises(ParameterNameError): + subject.validate_variable_name_unique("copy", {"paste", "copy", "cut"}) + + def test_ensure_display_name() -> None: """It should ensure the display name is within the character limit.""" result = subject.ensure_display_name("abc") @@ -96,10 +104,12 @@ def test_ensure_variable_name_raises_keyword(variable_name: str) -> None: def test_validate_options() -> None: """It should not raise when given valid constraints""" subject.validate_options(123, 1, 100, None, int) + subject.validate_options(123, 100, 100, None, int) subject.validate_options( 123, None, None, [{"display_name": "abc", "value": 456}], int ) subject.validate_options(12.3, 1.1, 100.9, None, float) + subject.validate_options(12.3, 1.1, 1.1, None, float) subject.validate_options( 12.3, None, None, [{"display_name": "abc", "value": 45.6}], float ) @@ -134,19 +144,84 @@ def test_validate_options_raises_name_error() -> None: [ (1.0, int, 1), (1.1, int, 1.1), + (2, float, 2.0), (2.0, float, 2.0), (2.2, float, 2.2), ("3.0", str, "3.0"), + (0.0, bool, False), + (1, bool, True), + (3.0, bool, 3.0), (True, bool, True), ], ) def test_ensure_value_type( value: Union[float, bool, str], param_type: type, result: AllowedTypes ) -> None: - """It should ensure the correct type is there, converting floats to ints.""" + """It should ensure that if applicable, the value is coerced into the expected type""" assert result == subject.ensure_value_type(value, param_type) +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + ], +) +def test_ensure_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_float_value(value) + + +@pytest.mark.parametrize( + ["value", "result"], + [ + (1, 1.0), + (2.0, 2.0), + (3.3, 3.3), + (None, None), + ], +) +def test_ensure_optional_float_value(value: Union[float, int], result: float) -> None: + """It should ensure that if applicable, the value is coerced into a float.""" + assert result == subject.ensure_optional_float_value(value) + + +@pytest.mark.parametrize( + ["choices", "result"], + [ + ([], []), + (None, None), + ( + [{"display_name": "foo", "value": 1}], + [{"display_name": "foo", "value": 1.0}], + ), + ( + [{"display_name": "foo", "value": 2.0}], + [{"display_name": "foo", "value": 2.0}], + ), + ( + [{"display_name": "foo", "value": 3.3}], + [{"display_name": "foo", "value": 3.3}], + ), + ( + [{"display_name": "foo", "value": "4"}], + [{"display_name": "foo", "value": "4"}], + ), + ( + [{"display_name": "foo", "value": True}], + [{"display_name": "foo", "value": True}], + ), + ], +) +def test_ensure_float_choices( + choices: Optional[List[ParameterChoice]], result: Optional[List[ParameterChoice]] +) -> None: + """It should ensure that if applicable, the value in a choice is coerced into a float.""" + assert result == subject.ensure_float_choices(choices) + + @pytest.mark.parametrize( ["param_type", "result"], [(int, "int"), (float, "float"), (str, "str")], @@ -206,14 +281,15 @@ def test_convert_type_string_for_num_param_raises(param_type: type) -> None: None, [{"display_name": "abc", "value": "123"}], int, - "must match type", + "must be of type", ), (123, 1, None, None, int, "maximum must also"), (123, None, 100, None, int, "minimum must also"), (123, 100, 1, None, int, "Maximum must be greater"), - (123, 1.1, 100, None, int, "Minimum and maximum must match type"), - (123, 1, 100.5, None, int, "Minimum and maximum must match type"), - (123, "1", "100", None, int, "Only parameters of type float or int"), + (123, 1.1, 100, None, int, "Minimum is type"), + (123, 1, 100.5, None, int, "Maximum is type"), + (123.0, "1.0", 100.0, None, float, "Minimum is type"), + ("blah", 1, 100, None, str, "Only parameters of type float or int"), ], ) def test_validate_options_raise_definition_error( diff --git a/api/tests/opentrons/test_legacy_broker.py b/api/tests/opentrons/test_legacy_broker.py index 2351f73e348c..719fe43052d8 100644 --- a/api/tests/opentrons/test_legacy_broker.py +++ b/api/tests/opentrons/test_legacy_broker.py @@ -2,8 +2,8 @@ from typing import List, NamedTuple, cast -from opentrons.commands.types import CommandMessage -from opentrons.commands.publisher import CommandPublisher, publish +from opentrons.legacy_commands.types import CommandMessage +from opentrons.legacy_commands.publisher import CommandPublisher, publish def _my_command(arg1: int, arg2: str = "", arg3: str = "") -> CommandMessage: diff --git a/api/tests/opentrons/util/test_performance_helpers.py b/api/tests/opentrons/util/test_performance_helpers.py new file mode 100644 index 000000000000..57a42ef6a714 --- /dev/null +++ b/api/tests/opentrons/util/test_performance_helpers.py @@ -0,0 +1,28 @@ +"""Tests for performance_helpers.""" + +from pathlib import Path +from opentrons_shared_data.performance.dev_types import RobotContextState +from opentrons.util.performance_helpers import ( + StubbedTracker, + _get_robot_context_tracker, +) + + +def test_return_function_unchanged() -> None: + """Test that the function is returned unchanged when using StubbedTracker.""" + tracker = StubbedTracker(Path("/path/to/storage"), True) + + def func_to_track() -> None: + pass + + assert ( + tracker.track(RobotContextState.ANALYZING_PROTOCOL)(func_to_track) + is func_to_track + ) + + +def test_singleton_tracker() -> None: + """Test that the tracker is a singleton.""" + tracker = _get_robot_context_tracker() + tracker2 = _get_robot_context_tracker() + assert tracker is tracker2 diff --git a/app-shell-odd/src/config/__fixtures__/index.ts b/app-shell-odd/src/config/__fixtures__/index.ts index 5e26ddc99ef9..b3ff0cbfbd74 100644 --- a/app-shell-odd/src/config/__fixtures__/index.ts +++ b/app-shell-odd/src/config/__fixtures__/index.ts @@ -11,11 +11,13 @@ import type { ConfigV21, } from '@opentrons/app/src/redux/config/types' +const PKG_VERSION: string = _PKG_VERSION_ + export const MOCK_CONFIG_V12: ConfigV12 = { version: 12, devtools: false, reinstallDevtools: false, - update: { channel: _PKG_VERSION_.includes('beta') ? 'beta' : 'latest' }, + update: { channel: PKG_VERSION.includes('beta') ? 'beta' : 'latest' }, log: { level: { file: 'debug', console: 'info' } }, ui: { width: 1024, diff --git a/app-shell-odd/src/config/migrate.ts b/app-shell-odd/src/config/migrate.ts index 6d9a1c9b82b5..9a05df795947 100644 --- a/app-shell-odd/src/config/migrate.ts +++ b/app-shell-odd/src/config/migrate.ts @@ -22,11 +22,12 @@ import type { const CONFIG_VERSION_LATEST = 21 // update this after each config version bump +const PKG_VERSION: string = _PKG_VERSION_ export const DEFAULTS_V12: ConfigV12 = { version: 12, devtools: false, reinstallDevtools: false, - update: { channel: _PKG_VERSION_.includes('beta') ? 'beta' : 'latest' }, + update: { channel: PKG_VERSION.includes('beta') ? 'beta' : 'latest' }, log: { level: { file: 'debug', console: 'info' } }, ui: { width: 1024, diff --git a/app-shell-odd/src/notifications/deserialize.ts b/app-shell-odd/src/notifications/deserialize.ts index 4539bc97faa5..01fd4bc933b9 100644 --- a/app-shell-odd/src/notifications/deserialize.ts +++ b/app-shell-odd/src/notifications/deserialize.ts @@ -12,7 +12,7 @@ import type { import { FAILURE_STATUSES } from '../constants' const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app-shell-odd/src/update.ts b/app-shell-odd/src/update.ts index f27ce2eced46..d1ea2f154b39 100644 --- a/app-shell-odd/src/update.ts +++ b/app-shell-odd/src/update.ts @@ -14,15 +14,15 @@ import type { ReleaseSetUrls } from './system-update/types' const log = createLogger('update') +const OPENTRONS_PROJECT: string = _OPENTRONS_PROJECT_ + export const FLEX_MANIFEST_URL = - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_ && - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_.includes('robot-stack') + OPENTRONS_PROJECT && OPENTRONS_PROJECT.includes('robot-stack') ? 'https://builds.opentrons.com/ot3-oe/releases.json' : 'https://ot3-development.builds.opentrons.com/ot3-oe/releases.json' -let LATEST_OT_SYSTEM_VERSION = _PKG_VERSION_ +const PKG_VERSION = _PKG_VERSION_ +let LATEST_OT_SYSTEM_VERSION = PKG_VERSION const channelFinder = (version: string, channel: string): boolean => { // return the latest alpha/beta if a user subscribes to alpha/beta updates @@ -60,7 +60,7 @@ export const updateLatestVersion = (): Promise => { }) .find(verson => channelFinder(verson, channel)) const changed = LATEST_OT_SYSTEM_VERSION !== latestAvailableVersion - LATEST_OT_SYSTEM_VERSION = latestAvailableVersion ?? _PKG_VERSION_ + LATEST_OT_SYSTEM_VERSION = latestAvailableVersion ?? PKG_VERSION if (changed) { log.info( `Update: latest version available from ${FLEX_MANIFEST_URL} is ${latestAvailableVersion}` @@ -80,7 +80,7 @@ export const getLatestVersion = (): string => { return LATEST_OT_SYSTEM_VERSION } -export const getCurrentVersion = (): string => _PKG_VERSION_ +export const getCurrentVersion = (): string => PKG_VERSION export const isUpdateAvailable = (): boolean => getLatestVersion() !== getCurrentVersion() diff --git a/app-shell-odd/typings/global.d.ts b/app-shell-odd/typings/global.d.ts index 8513596d0457..3b470870c2be 100644 --- a/app-shell-odd/typings/global.d.ts +++ b/app-shell-odd/typings/global.d.ts @@ -1,11 +1,4 @@ -import type { IpcRenderer } from 'electron' - declare global { - const _PKG_VERSION_: string - const _PKG_PRODUCT_NAME_: string - const _PKG_BUGS_URL_: string - const _OPENTRONS_PROJECT_: string - namespace NodeJS { export interface Global { APP_SHELL_REMOTE: { @@ -14,3 +7,8 @@ declare global { } } } + +declare const _PKG_VERSION_: string +declare const _PKG_PRODUCT_NAME_: string +declare const _PKG_BUGS_URL_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/app-shell-odd/vite.config.ts b/app-shell-odd/vite.config.ts index b95751596755..7848c92bd8d3 100644 --- a/app-shell-odd/vite.config.ts +++ b/app-shell-odd/vite.config.ts @@ -1,13 +1,14 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import type { UserConfig } from 'vite' export default defineConfig( async (): Promise => { @@ -79,7 +80,7 @@ export default defineConfig( '../discovery-client/src/index.ts' ), '@opentrons/usb-bridge/node-client': path.resolve( - '../usb-bridge/node-client/src/inxex.ts' + '../usb-bridge/node-client/src/index.ts' ), }, }, diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index a15d877c0aba..591aa411a3ca 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,34 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 1.4.0-alpha.1 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + +### Notable bug fixes + +App and robot update prompts should now function properly. However, updating from 1.4.0-alpha.0 to 1.4.0-alpha.1 will still present issues, as the fix is not in 1.4.0-alpha.0. After installing 1.4.0-alpha.1, switch your update channel to "latest" to receive the latest stable internal release prompt, which validates the fix. + +### All changes + + + +--- + +## Internal Release 1.4.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + +--- + +## Internal Release 1.3.0-alpha.0 + +This internal release is from the `edge` branch to contain rapid dev on new features for 7.3.0. This release is for internal testing purposes and if used may require a factory reset of the robot to return to a stable version. + + + --- # Internal Release 1.1.0 diff --git a/app-shell/build/release-notes.md b/app-shell/build/release-notes.md index 97fa5f01b81b..43db1bdfaf8d 100644 --- a/app-shell/build/release-notes.md +++ b/app-shell/build/release-notes.md @@ -6,6 +6,14 @@ log][]. For a list of currently known issues, please see the [Opentrons issue tr --- +## Opentrons App Changes in 7.2.2 + +Welcome to the v7.2.2 release of the Opentrons App! + +There are no changes to the Opentrons App in v7.2.2, but it is required for updating the robot software to improve some features. + +--- + ## Opentrons App Changes in 7.2.1 Welcome to the v7.2.1 release of the Opentrons App! diff --git a/app-shell/electron-builder.config.js b/app-shell/electron-builder.config.js index 727b2d5e9004..aa61720338b4 100644 --- a/app-shell/electron-builder.config.js +++ b/app-shell/electron-builder.config.js @@ -1,6 +1,5 @@ 'use strict' const path = require('path') -const { versionForProject } = require('../scripts/git-version') const { OT_APP_DEPLOY_BUCKET, @@ -45,7 +44,9 @@ module.exports = async () => ({ }, ], extraMetadata: { - version: await versionForProject(project), + version: await ( + await import('../scripts/git-version.mjs') + ).versionForProject(project), productName: project === 'robot-stack' ? 'Opentrons' : 'Opentrons-OT3', }, extraResources: USE_PYTHON ? ['python'] : [], diff --git a/app-shell/src/menu.ts b/app-shell/src/menu.ts index 71b1318df38c..52f049789346 100644 --- a/app-shell/src/menu.ts +++ b/app-shell/src/menu.ts @@ -5,6 +5,9 @@ import type { MenuItemConstructorOptions } from 'electron' import { LOG_DIR } from './log' +const PRODUCT_NAME: string = _PKG_PRODUCT_NAME_ +const BUGS_URL: string = _PKG_BUGS_URL_ + // file or application menu const firstMenu: MenuItemConstructorOptions = { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu', @@ -27,8 +30,7 @@ const helpMenu: MenuItemConstructorOptions = { }, }, { - // @ts-expect-error can't get TS to recognize global.d.ts - label: `View ${global._PKG_PRODUCT_NAME_} App Logs`, + label: `View ${PRODUCT_NAME} App Logs`, click: () => { shell.openPath(LOG_DIR) }, @@ -37,8 +39,7 @@ const helpMenu: MenuItemConstructorOptions = { label: 'Report an Issue', click: () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - // @ts-expect-error can't get TS to recognize global.d.ts - shell.openExternal(global._PKG_BUGS_URL_) + shell.openExternal(BUGS_URL) }, }, ], diff --git a/app-shell/src/notifications/__tests__/deserialize.test.ts b/app-shell/src/notifications/__tests__/deserialize.test.ts index 9c6642d3931a..ca9bab984fbc 100644 --- a/app-shell/src/notifications/__tests__/deserialize.test.ts +++ b/app-shell/src/notifications/__tests__/deserialize.test.ts @@ -4,7 +4,7 @@ import { deserializeExpectedMessages } from '../deserialize' import type { NotifyResponseData } from '@opentrons/app/src/redux/shell/types' -const MOCK_VALID_RESPONSE: NotifyResponseData = { refetchUsingHTTP: true } +const MOCK_VALID_RESPONSE: NotifyResponseData = { refetch: true } const MOCK_VALID_STRING_RESPONSE = JSON.stringify(MOCK_VALID_RESPONSE) const MOCK_INVALID_OBJECT = JSON.stringify({ test: 'MOCK_RESPONSE' }) const MOCK_INVALID_STRING = 'MOCK_STRING' diff --git a/app-shell/src/notifications/__tests__/store.test.ts b/app-shell/src/notifications/__tests__/store.test.ts new file mode 100644 index 000000000000..7192c8c2fa09 --- /dev/null +++ b/app-shell/src/notifications/__tests__/store.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, beforeEach } from 'vitest' + +import { connectionStore } from '../store' + +const MOCK_IP = 'MOCK_IP' +const MOCK_ROBOT = 'MOCK_ROBOT' +const MOCK_WINDOW = {} as any +const MOCK_CLIENT = { connected: true } as any +const MOCK_TOPIC = 'MOCK_TOPIC' as any + +describe('ConnectionStore', () => { + beforeEach(() => { + connectionStore.clearStore() + }) + + describe('getBrowserWindow', () => { + it('should return the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('getAllBrokersInStore', () => { + it('should return an empty array if there are no brokers in the store', () => { + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + }) + + it('should return an array of broker names in the store', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setPendingConnection('robot2') + expect(connectionStore.getAllBrokersInStore()).toEqual([ + MOCK_ROBOT, + 'robot2', + ]) + }) + }) + + describe('getClient', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getClient(MOCK_IP)).toBeNull() + }) + + it('should return the client if the given IP is associated with a connection', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + }) + + describe('setErrorStatus and getFailedConnectionStatus', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBeNull() + }) + + it('should return the unreachable status for the given IP', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNFAILED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should return "ECONNFAILED" if the unreachable status for the given IP is "ECONNREFUSED" after the first error status check', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNREFUSED' + ) + expect(connectionStore.getFailedConnectionStatus(MOCK_IP)).toBe( + 'ECONNFAILED' + ) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + ).rejects.toThrowError('MOCK_IP is not associated with a connection') + }) + }) + + describe('getRobotNameByIP', () => { + it('should return null if the given IP is not associated with a connection', () => { + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBeNull() + }) + + it('should return the robot name associated with the given IP', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + }) + + describe('setBrowserWindow', () => { + it('should set the browser window', () => { + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getBrowserWindow()).toBe(MOCK_WINDOW) + }) + }) + + describe('setPendingConnection', () => { + it('should create a new connection if there is no connection currently connecting', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.getAllBrokersInStore()).toEqual([MOCK_ROBOT]) + }) + + it('should reject with an error if there is already a connection currently connecting', async () => { + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).resolves.toBeUndefined() + await expect( + connectionStore.setPendingConnection(MOCK_ROBOT) + ).rejects.toThrowError( + 'Cannot create a new connection while currently connecting.' + ) + }) + }) + + describe('setConnected', () => { + it('should set the client for the given robot name', async () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getClient(MOCK_IP)).toBe(MOCK_CLIENT) + }) + + it('should reject with an error if there is already a connection for the given robot name', async () => { + const MOCK_CLIENT_2 = {} as any + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT_2) + ).rejects.toThrowError('Connection already exists for MOCK_ROBOT') + }) + + it('should reject with an error if the given robot name is not associated with a connection', async () => { + await expect( + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setSubStatus', () => { + it('should set the pending sub status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the subscribed status for the given IP and topic', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should throw an error if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('setUnsubStatus', () => { + it('should set the pending unsub status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + + it('should set the unsubscribed status for the given IP and topic if it is currently subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'unsubscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should not do anything if the given IP is not associated with a connection', async () => { + await expect( + connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + ).rejects.toThrowError('IP is not associated with a connection') + }) + }) + + describe('associateIPWithRobotName', () => { + it('should associate the given IP with the given robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe(MOCK_ROBOT) + }) + + it('should update the association if the IP is already associated with a different robot name', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + connectionStore.associateIPWithRobotName(MOCK_IP, 'robot2') + expect(connectionStore.getRobotNameByIP(MOCK_IP)).toBe('robot2') + }) + }) + + describe('clearStore', () => { + it('should clear all connections and robot names', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setBrowserWindow(MOCK_WINDOW) + expect(connectionStore.getAllBrokersInStore()).not.toEqual([]) + expect(connectionStore.getBrowserWindow()).not.toBeNull() + connectionStore.clearStore() + expect(connectionStore.getAllBrokersInStore()).toEqual([]) + expect(connectionStore.getBrowserWindow()).toBeNull() + }) + }) + + describe('isConnectedToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is not null', async () => { + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectedToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isConnectingToBroker', () => { + it('should return false if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return false if the connection client is not null', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(false) + }) + + it('should return true if the connection client is null and the connection is not terminated', () => { + connectionStore.setPendingConnection(MOCK_ROBOT) + expect(connectionStore.isConnectingToBroker(MOCK_ROBOT)).toBe(true) + }) + }) + + describe('isPendingSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isActiveSub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not subscribed', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is subscribed', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + expect(connectionStore.isActiveSub(MOCK_ROBOT, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isPendingUnsub', () => { + it('should return false if the given IP is not associated with a connection', () => { + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return false if the topic is not pending', () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(false) + }) + + it('should return true if the topic is pending', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setSubStatus(MOCK_IP, MOCK_TOPIC, 'subscribed') + await connectionStore.setUnsubStatus(MOCK_IP, MOCK_TOPIC, 'pending') + expect(connectionStore.isPendingUnsub(MOCK_IP, MOCK_TOPIC)).toBe(true) + }) + }) + + describe('isConnectionTerminated', () => { + it('should return true if the given robot name is not associated with a connection', () => { + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return true if the unreachable status is not null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + await connectionStore.setErrorStatus(MOCK_IP, 'Connection refused') + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(true) + }) + + it('should return false if the unreachable status is null', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + await connectionStore.setConnected(MOCK_ROBOT, MOCK_CLIENT) + expect(connectionStore.isConnectionTerminated(MOCK_ROBOT)).toBe(false) + }) + }) + + describe('isKnownPortBlockedIP', () => { + it('should return false if the given IP is not in the known port blocked IPs set', () => { + expect(connectionStore.isKnownPortBlockedIP('MOCK_IP_2')).toBe(false) + }) + + it('should return true if the given IP is in the known port blocked IPs set', async () => { + connectionStore.associateIPWithRobotName(MOCK_IP, MOCK_ROBOT) + await connectionStore.setPendingConnection(MOCK_ROBOT) + connectionStore.setErrorStatus(MOCK_IP, 'ECONNREFUSED') + expect(connectionStore.isKnownPortBlockedIP(MOCK_IP)).toBe(true) + }) + }) +}) diff --git a/app-shell/src/notifications/deserialize.ts b/app-shell/src/notifications/deserialize.ts index c96d6d192034..53752b32a0fb 100644 --- a/app-shell/src/notifications/deserialize.ts +++ b/app-shell/src/notifications/deserialize.ts @@ -18,7 +18,7 @@ interface SendToBrowserParams { } const VALID_NOTIFY_RESPONSES: [NotifyRefetchData, NotifyUnsubscribeData] = [ - { refetchUsingHTTP: true }, + { refetch: true }, { unsubscribe: true }, ] diff --git a/app-shell/src/notifications/store.ts b/app-shell/src/notifications/store.ts index 9968080258ef..c9742ec6f90e 100644 --- a/app-shell/src/notifications/store.ts +++ b/app-shell/src/notifications/store.ts @@ -207,7 +207,8 @@ class ConnectionStore { public isConnectingToBroker(robotName: string): boolean { return ( - (this.hostsByRobotName[robotName]?.client == null ?? false) && + robotName in this.hostsByRobotName && + this.hostsByRobotName[robotName].client == null && !this.isConnectionTerminated(robotName) ) } diff --git a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts index 2c4d5a911ae3..4514887cb6dd 100644 --- a/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts +++ b/app-shell/src/protocol-analysis/__tests__/writeFailedAnalysis.test.ts @@ -41,6 +41,7 @@ describe('write failed analysis', () => { modules: [], pipettes: [], liquids: [], + runTimeParameters: [], }) }) }) diff --git a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts index 519184a3d417..8723cd52d043 100644 --- a/app-shell/src/protocol-analysis/writeFailedAnalysis.ts +++ b/app-shell/src/protocol-analysis/writeFailedAnalysis.ts @@ -27,6 +27,7 @@ export function createFailedAnalysis( pipettes: [], modules: [], liquids: [], + runTimeParameters: [], // TODO(mc, 2022-05-04): this field does not make sense for an // analysis that was unable to complete, but is required by // ProtocolAnalysisOutput diff --git a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts index c873f47242c5..3ac1a106dbe0 100644 --- a/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts +++ b/app-shell/src/protocol-storage/__tests__/protocol-storage.test.ts @@ -119,6 +119,7 @@ describe('protocol storage directory utilities', () => { pipettes: [], modules: [], labware: [], + runTimeParameters: [], }) }) }) diff --git a/app-shell/src/robot-update/constants.ts b/app-shell/src/robot-update/constants.ts index 48f8ef8e6110..22a494d07d74 100644 --- a/app-shell/src/robot-update/constants.ts +++ b/app-shell/src/robot-update/constants.ts @@ -4,6 +4,8 @@ import type { UpdateManifestUrls } from './types' import type { RobotUpdateTarget } from '@opentrons/app/src/redux/robot-update/types' import { CURRENT_VERSION } from '../update' +const OPENTRONS_PROJECT: string = _OPENTRONS_PROJECT_ + const UPDATE_MANIFEST_URLS_RELEASE = { ot2: 'https://builds.opentrons.com/ot2-br/releases.json', flex: 'https://builds.opentrons.com/ot3-oe/releases.json', @@ -15,8 +17,7 @@ const UPDATE_MANIFEST_URLS_INTERNAL_RELEASE = { } export const getUpdateManifestUrls = (): UpdateManifestUrls => - // @ts-expect-error can't get TS to recognize global.d.ts - global._OPENTRONS_PROJECT_.includes('robot-stack') + OPENTRONS_PROJECT.includes('robot-stack') ? UPDATE_MANIFEST_URLS_RELEASE : UPDATE_MANIFEST_URLS_INTERNAL_RELEASE diff --git a/app-shell/typings/global.d.ts b/app-shell/typings/global.d.ts index 8bdea90e6376..67f9a5a19554 100644 --- a/app-shell/typings/global.d.ts +++ b/app-shell/typings/global.d.ts @@ -1,8 +1,9 @@ /* eslint-disable no-var */ declare global { - var _PKG_VERSION_: string - var _PKG_PRODUCT_NAME_: string - var _PKG_BUGS_URL_: string - var _OPENTRONS_PROJECT_: string var APP_SHELL_REMOTE: { ipcRenderer: IpcRenderer; [key: string]: any } } + +declare const _PKG_VERSION_: string +declare const _PKG_PRODUCT_NAME_: string +declare const _PKG_BUGS_URL_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/app-shell/vite.config.ts b/app-shell/vite.config.ts index 80ca80b0aa45..546fe19e23f3 100644 --- a/app-shell/vite.config.ts +++ b/app-shell/vite.config.ts @@ -1,7 +1,8 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' +import type { UserConfig } from 'vite' export default defineConfig( async (): Promise => { diff --git a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py index d9f4f62970a8..fdb7c172256c 100644 --- a/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py +++ b/app-testing/files/protocols/py/OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3.py @@ -3,23 +3,370 @@ from opentrons import protocol_api metadata = { - "protocolName": "🛠️ 2.17 Smoke Test", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "author": "Opentrons Engineering ", "source": "Software Testing Team", - "description": ("Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test."), + "description": ("Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ "), } -requirements = {"robotType": "OT-2", "apiLevel": "2.16"} +requirements = {"robotType": "OT-2", "apiLevel": "2.17"} + + +######################### +#### LOOK AT THIS ####### +######################### + +# This protocol is exactly the same as 2.16 Smoke Test V3 +# The only difference is the API version in the metadata +# There were no new positive test cases for 2.17 +# The negative test cases are captured in the 2.17 dispense changes protcol + +######################### +#### LOOK AT THIS ####### +######################### def run(ctx: protocol_api.ProtocolContext) -> None: """This method is run by the protocol engine.""" - # The only change in api version 2.17 is an error is thrown when you try to dispense more than the current volume of liquid in the pipette. - # Since the smoke test protocol should be able to be ran through without any errors, the test for the dispense error should not be added to the smoke test protocol. + ctx.set_rail_lights(True) + ctx.comment(f"Let there be light! {ctx.rail_lights_on} 🌠🌠🌠") + ctx.comment(f"Is the door is closed? {ctx.door_closed} 🚪🚪🚪") + ctx.comment(f"Is this a simulation? {ctx.is_simulating()} 🔮🔮🔮") + ctx.comment(f"Running against API Version: {ctx.api_version}") + + # deck positions + tips_300ul_position = "5" + tips_20ul_position = "4" + dye_source_position = "3" + logo_position = "2" + temperature_position = "9" + custom_lw_position = "6" + hs_position = "1" + + # Thermocycler has a default position that covers Slots 7, 8, 10, and 11. + # This is the only valid location for the Thermocycler on the OT-2 deck. + # This position is a default parameter when declaring the TC so you do not need to specify. + + # 300ul tips + tips_300ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_300ul", + location=tips_300ul_position, + label="300ul tips", + ) + ] + + # 20ul tips + tips_20ul = [ + ctx.load_labware( + load_name="opentrons_96_tiprack_20ul", + location=tips_20ul_position, + label="20ul tips", + ) + ] + + # pipettes + pipette_left = ctx.load_instrument(instrument_name="p300_multi_gen2", mount="left", tip_racks=tips_300ul) + + pipette_right = ctx.load_instrument(instrument_name="p20_single_gen2", mount="right", tip_racks=tips_20ul) + + # modules https://docs.opentrons.com/v2/new_modules.html#available-modules + hs_module = ctx.load_module("heaterShakerModuleV1", hs_position) + temperature_module = ctx.load_module("temperature module gen2", temperature_position) + thermocycler_module = ctx.load_module("thermocycler module gen2") + + # module labware + temp_adapter = temperature_module.load_adapter("opentrons_96_well_aluminum_block") + temp_plate = temp_adapter.load_labware( + "nest_96_wellplate_100ul_pcr_full_skirt", + label="Temperature-Controlled plate", + ) + hs_plate = hs_module.load_labware(name="nest_96_wellplate_100ul_pcr_full_skirt", adapter="opentrons_96_pcr_adapter") + tc_plate = thermocycler_module.load_labware("nest_96_wellplate_100ul_pcr_full_skirt") + + # A 2.14 difference, no params specified, still should find it. + custom_labware = ctx.load_labware( + "cpx_4_tuberack_100ul", + custom_lw_position, + label="4 custom tubes", + ) + + # create plates and pattern list + logo_destination_plate = ctx.load_labware( + load_name="nest_96_wellplate_100ul_pcr_full_skirt", + location=logo_position, + label="logo destination", + ) + + dye_container = ctx.load_labware( + load_name="nest_12_reservoir_15ml", + location=dye_source_position, + label="dye container", + ) + + dye_source = dye_container.wells_by_name()["A2"] + + # Well Location set-up + dye_destination_wells = [ + logo_destination_plate.wells_by_name()["C7"], + logo_destination_plate.wells_by_name()["D6"], + logo_destination_plate.wells_by_name()["D7"], + logo_destination_plate.wells_by_name()["D8"], + logo_destination_plate.wells_by_name()["E5"], + ] + + # >= 2.14 define_liquid and load_liquid + water = ctx.define_liquid( + name="water", description="H₂O", display_color="#42AB2D" + ) # subscript 2 https://www.compart.com/en/unicode/U+2082 + + acetone = ctx.define_liquid( + name="acetone", description="C₃H₆O", display_color="#38588a" + ) # subscript 3 https://www.compart.com/en/unicode/U+2083 + # subscript 6 https://www.compart.com/en/unicode/U+2086 + + dye_container.wells_by_name()["A1"].load_liquid(liquid=water, volume=4000) + dye_container.wells_by_name()["A2"].load_liquid(liquid=water, volume=2000) + dye_container.wells_by_name()["A5"].load_liquid(liquid=acetone, volume=555.55555) + + # 2 different liquids in the same well + dye_container.wells_by_name()["A8"].load_liquid(liquid=water, volume=900.00) + dye_container.wells_by_name()["A8"].load_liquid(liquid=acetone, volume=1001.11) + + hs_module.close_labware_latch() + + pipette_right.pick_up_tip() + + ################################## + # Manual Deck State Modification # + ################################## + + # -------------------------- # + # Added in API version: 2.15 # + # -------------------------- # + + # Putting steps for this at beginning of protocol so you can do the manual stuff + # then walk away to let the rest of the protocol execute + + # The test flow is as follows: + # 1. Remove the existing PCR plate from slot 2 + # 2. Move the reservoir from slot 3 to slot 2 + # 3. Pickup P20 tip, move pipette to reservoir A1 in slot 2 + # 4. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 2 + # 5. Move the reservoir back to slot 3 from slot 2 + # 6. Move pipette to reservoir A1 in slot 3 + # 7. Pause and ask user to validate that the tip is in the middle of reservoir A1 in slot 3 + # 8. Move custom labware from slot 6 to slot 2 + # 9. Move pipette to well A1 in slot 2 + # 10. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + # 11. Move the custom labware back to slot 6 from slot 2 + # 12. Move pipette to well A1 in slot 6 + # 13. Pause and ask user to validate that the tip is in the middle of well A1 in slot 6 + # 14. Move the offdeck PCR plate back to slot 2 + # 15. Move pipette to well A1 in slot 2 + # 16. Pause and ask user to validate that the tip is in the middle of well A1 in slot 2 + + # In effect, nothing will actually change to the protocol, + # but we will be able to test that the UI responds appropriately. + + # Note: + # logo_destination_plate is a nest_96_wellplate_100ul_pcr_full_skirt - starting position is slot 2 + # dye_container is a nest_12_reservoir_15ml - starting position is slot 3 + + # Step 1 + ctx.move_labware( + labware=logo_destination_plate, + new_location=protocol_api.OFF_DECK, + ) + + # Step 2 + ctx.move_labware(labware=dye_container, new_location="2") + + # Step 3 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 4 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 2?") + + # Step 5 + ctx.move_labware(labware=dye_container, new_location="3") + + # Step 6 + pipette_right.move_to(location=dye_container.wells_by_name()["A1"].top()) + + # Step 7 + ctx.pause("Is the pipette tip in the middle of reservoir A1 in slot 3?") + + # Step 8 + ctx.move_labware(labware=custom_labware, new_location="2") + + # Step 9 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 10 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 2?") + + # Step 11 + ctx.move_labware(labware=custom_labware, new_location="6") + + # Step 12 + pipette_right.move_to(location=custom_labware.wells_by_name()["A1"].top()) + + # Step 13 + ctx.pause("Is the pipette tip in the middle of custom labware A1 in slot 6?") + + # Step 14 + ctx.move_labware(labware=logo_destination_plate, new_location="2") + + # Step 15 + pipette_right.move_to(location=logo_destination_plate.wells_by_name()["A1"].top()) + + # Step 16 + ctx.pause("Is the pipette tip in the middle of well A1 in slot 2?") + + ####################### + # prepare_to_aspirate # + ####################### + + # -------------------------- # + # Added in API version: 2.16 # + # -------------------------- # + + pipette_right.prepare_to_aspirate() + pipette_right.move_to(dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause( + "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + ) + pipette_right.aspirate(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + ctx.pause("Did the pipette move up out of the well, only once, after aspirating?") + pipette_right.dispense(10, dye_container.wells_by_name()["A1"].bottom(z=2)) + + ######################################### + # protocol_context.fixed_trash property # + ######################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(ctx.fixed_trash) + ctx.pause("Is the pipette over the trash? Pipette will home after this pause.") + ctx.home() + + ############################################### + # instrument_context.trash_container property # + ############################################### + + # ---------------------------- # + # Changed in API version: 2.16 # + # ---------------------------- # + + pipette_right.move_to(pipette_right.trash_container) + ctx.pause("Is the pipette over the trash?") + + # Distribute dye + pipette_right.distribute( + volume=18, + source=dye_source, + dest=dye_destination_wells, + new_tip="never", + ) + pipette_right.drop_tip() + + # transfer + transfer_destinations = [ + logo_destination_plate.wells_by_name()["A11"], + logo_destination_plate.wells_by_name()["B11"], + logo_destination_plate.wells_by_name()["C11"], + ] + pipette_right.pick_up_tip() + pipette_right.transfer( + volume=60, + source=dye_container.wells_by_name()["A2"], + dest=transfer_destinations, + new_tip="never", + touch_tip=True, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + mix_after=(1, 20), + mix_touch_tip=True, + ) + + # consolidate + pipette_right.consolidate( + volume=20, + source=transfer_destinations, + dest=dye_container.wells_by_name()["A5"], + new_tip="never", + touch_tip=False, + blow_out=True, + blowout_location="destination well", + mix_before=(3, 20), + ) + + # well to well + pipette_right.return_tip() + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=5, location=logo_destination_plate.wells_by_name()["A11"]) + pipette_right.air_gap(volume=10) + ctx.delay(seconds=3) + pipette_right.dispense(volume=5, location=logo_destination_plate.wells_by_name()["H11"]) + + # move to + pipette_right.move_to(logo_destination_plate.wells_by_name()["E12"].top()) + pipette_right.move_to(logo_destination_plate.wells_by_name()["E11"].bottom()) + pipette_right.blow_out() + # touch tip + # pipette ends in the middle of the well as of 6.3.0 in all touch_tip + pipette_right.touch_tip(location=logo_destination_plate.wells_by_name()["H1"]) + ctx.pause("Is the pipette tip in the middle of the well?") + pipette_right.return_tip() + + # Play with the modules + temperature_module.await_temperature(25) + + hs_module.set_and_wait_for_shake_speed(466) + ctx.delay(seconds=5) + + hs_module.set_and_wait_for_temperature(38) + + thermocycler_module.open_lid() + thermocycler_module.close_lid() + thermocycler_module.set_lid_temperature(38) # 37 is the minimum + thermocycler_module.set_block_temperature(temperature=28, hold_time_seconds=5) + thermocycler_module.deactivate_block() + thermocycler_module.deactivate_lid() + thermocycler_module.open_lid() + + hs_module.deactivate_shaker() + + # dispense to modules + + # to temperature module + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=15, location=dye_source) + pipette_right.dispense(volume=15, location=temp_plate.well(0)) + pipette_right.drop_tip() - # Instead it should be added to a separate test protocol - OT2_P300M_P20S_TC_HS_TM_2_17_dispense_changes.py + # to heater shaker + pipette_left.pick_up_tip() + pipette_left.aspirate(volume=50, location=dye_source) + pipette_left.dispense(volume=50, location=hs_plate.well(0)) + hs_module.set_and_wait_for_shake_speed(350) + ctx.delay(seconds=5) + hs_module.deactivate_shaker() - # Therefore the 2.17 smoke test protocol is the same as the 2.16 smoke test protocol. Instead of copying and pasting the 2.16 smoke test protocol, we will noop this protocol and add a comment to explain the situation. + # to custom labware + # This labware does not EXIST!!!! so... + # Use tip rack lid to catch dye on wet run + pipette_right.pick_up_tip() + pipette_right.aspirate(volume=10, location=dye_source, rate=2.0) + pipette_right.dispense(volume=10, location=custom_labware.well(3), rate=1.5) + pipette_right.drop_tip() - pass + # to thermocycler + pipette_left.aspirate(volume=75, location=dye_source) + pipette_left.dispense(volume=60, location=tc_plate.wells_by_name()["A6"]) + pipette_left.drop_tip() diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json index d321c3f1579f..69b643fbc46b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0400decc88][Flex_P1000MLeft_P50MRight_HS_TM_MM_TC_2_15_ABR4_Illumina_DNA_Prep_24x].json @@ -8513,7 +8513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8538,7 +8538,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8563,7 +8563,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8588,7 +8588,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8613,7 +8613,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8639,7 +8639,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8664,7 +8664,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8689,7 +8689,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8715,7 +8715,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8740,7 +8740,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8765,7 +8765,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8791,7 +8791,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -8816,7 +8816,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8841,7 +8841,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -8866,7 +8866,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -8890,7 +8890,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -8924,7 +8924,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -8938,7 +8938,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9195,7 +9195,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9220,7 +9220,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9245,7 +9245,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9270,7 +9270,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9295,7 +9295,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9321,7 +9321,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9346,7 +9346,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9371,7 +9371,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9397,7 +9397,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9422,7 +9422,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9447,7 +9447,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9473,7 +9473,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9498,7 +9498,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9523,7 +9523,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9548,7 +9548,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -9572,7 +9572,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9606,7 +9606,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -9620,7 +9620,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -9877,7 +9877,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9902,7 +9902,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9927,7 +9927,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -9952,7 +9952,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -9977,7 +9977,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10003,7 +10003,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10028,7 +10028,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10053,7 +10053,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10079,7 +10079,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10104,7 +10104,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10129,7 +10129,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10155,7 +10155,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -10180,7 +10180,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10205,7 +10205,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -11.280000000000001 + "z": -11.279999999999998 }, "origin": "top" }, @@ -10230,7 +10230,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -10254,7 +10254,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -10288,7 +10288,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -10302,7 +10302,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -10534,7 +10534,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -10559,7 +10559,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -10584,7 +10584,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10610,7 +10610,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10636,7 +10636,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10662,7 +10662,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10688,7 +10688,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10714,7 +10714,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10740,7 +10740,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10766,7 +10766,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10792,7 +10792,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10818,7 +10818,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10844,7 +10844,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10870,7 +10870,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10896,7 +10896,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10922,7 +10922,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10948,7 +10948,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -10974,7 +10974,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11000,7 +11000,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11026,7 +11026,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11052,7 +11052,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11078,7 +11078,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11180,7 +11180,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -11205,7 +11205,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -11230,7 +11230,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11256,7 +11256,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11282,7 +11282,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11308,7 +11308,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11334,7 +11334,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11360,7 +11360,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11386,7 +11386,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11412,7 +11412,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11438,7 +11438,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11464,7 +11464,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11490,7 +11490,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11516,7 +11516,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11542,7 +11542,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11568,7 +11568,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11594,7 +11594,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11620,7 +11620,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11646,7 +11646,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11672,7 +11672,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11698,7 +11698,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11724,7 +11724,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11826,7 +11826,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -11851,7 +11851,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -11876,7 +11876,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11902,7 +11902,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11928,7 +11928,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11954,7 +11954,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -11980,7 +11980,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12006,7 +12006,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12032,7 +12032,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12058,7 +12058,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12084,7 +12084,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12110,7 +12110,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12136,7 +12136,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12162,7 +12162,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12188,7 +12188,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12214,7 +12214,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12240,7 +12240,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12266,7 +12266,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12292,7 +12292,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12318,7 +12318,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12344,7 +12344,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -12370,7 +12370,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -13428,7 +13428,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13453,7 +13453,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13479,7 +13479,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13505,7 +13505,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13531,7 +13531,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13557,7 +13557,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13592,7 +13592,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13625,7 +13625,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13650,7 +13650,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -13741,7 +13741,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13766,7 +13766,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13792,7 +13792,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13818,7 +13818,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13844,7 +13844,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13870,7 +13870,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -13905,7 +13905,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13938,7 +13938,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -13963,7 +13963,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -14054,7 +14054,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14079,7 +14079,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14105,7 +14105,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14131,7 +14131,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14157,7 +14157,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14183,7 +14183,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -14218,7 +14218,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -14251,7 +14251,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -14276,7 +14276,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15315,7 +15315,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15340,7 +15340,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15366,7 +15366,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15392,7 +15392,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15418,7 +15418,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15444,7 +15444,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15479,7 +15479,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15512,7 +15512,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15537,7 +15537,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15628,7 +15628,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15653,7 +15653,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15679,7 +15679,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15705,7 +15705,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15731,7 +15731,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15757,7 +15757,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15792,7 +15792,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15825,7 +15825,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -15850,7 +15850,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -15941,7 +15941,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15966,7 +15966,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -15992,7 +15992,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16018,7 +16018,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16044,7 +16044,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16070,7 +16070,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -16105,7 +16105,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -16138,7 +16138,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -16163,7 +16163,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17202,7 +17202,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17227,7 +17227,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17253,7 +17253,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17279,7 +17279,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17305,7 +17305,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17331,7 +17331,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17366,7 +17366,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17399,7 +17399,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17424,7 +17424,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17515,7 +17515,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17540,7 +17540,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17566,7 +17566,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17592,7 +17592,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17618,7 +17618,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17644,7 +17644,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17679,7 +17679,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17712,7 +17712,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -17737,7 +17737,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -17828,7 +17828,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17853,7 +17853,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17879,7 +17879,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17905,7 +17905,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17931,7 +17931,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17957,7 +17957,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -17992,7 +17992,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -18025,7 +18025,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 } }, "status": "succeeded" @@ -18050,7 +18050,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 35.650000000000006 + "z": 35.65 }, "volume": 20.0 }, @@ -19417,7 +19417,7 @@ "offset": { "x": 1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19442,7 +19442,7 @@ "offset": { "x": 1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19467,7 +19467,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19492,7 +19492,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19517,7 +19517,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19542,7 +19542,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19567,7 +19567,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19592,7 +19592,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19617,7 +19617,7 @@ "offset": { "x": -1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19642,7 +19642,7 @@ "offset": { "x": -1.040000000000001, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19667,7 +19667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19692,7 +19692,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19717,7 +19717,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19742,7 +19742,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -19767,7 +19767,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19792,7 +19792,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19818,7 +19818,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -19843,7 +19843,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -19867,7 +19867,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -19901,7 +19901,7 @@ "position": { "x": 14.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -19915,7 +19915,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20015,7 +20015,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20040,7 +20040,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20065,7 +20065,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20090,7 +20090,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20115,7 +20115,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20140,7 +20140,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20165,7 +20165,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20190,7 +20190,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20215,7 +20215,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20240,7 +20240,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20265,7 +20265,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20290,7 +20290,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20315,7 +20315,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20340,7 +20340,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20365,7 +20365,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20390,7 +20390,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20416,7 +20416,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20441,7 +20441,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -20465,7 +20465,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20499,7 +20499,7 @@ "position": { "x": 23.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -20513,7 +20513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -20613,7 +20613,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20638,7 +20638,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20663,7 +20663,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20688,7 +20688,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20713,7 +20713,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20738,7 +20738,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20763,7 +20763,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20788,7 +20788,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20813,7 +20813,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20838,7 +20838,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20863,7 +20863,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20888,7 +20888,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20913,7 +20913,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20938,7 +20938,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -20963,7 +20963,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -20988,7 +20988,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -21014,7 +21014,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -21039,7 +21039,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -21063,7 +21063,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -21097,7 +21097,7 @@ "position": { "x": 32.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -21111,7 +21111,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -21311,7 +21311,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21337,7 +21337,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21363,7 +21363,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21389,7 +21389,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21415,7 +21415,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21441,7 +21441,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21467,7 +21467,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21569,7 +21569,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21595,7 +21595,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21621,7 +21621,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21647,7 +21647,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21673,7 +21673,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21699,7 +21699,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21725,7 +21725,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21827,7 +21827,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21853,7 +21853,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21879,7 +21879,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21905,7 +21905,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21931,7 +21931,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21957,7 +21957,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -21983,7 +21983,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 10.0 }, @@ -22307,7 +22307,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22333,7 +22333,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22359,7 +22359,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22461,7 +22461,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22487,7 +22487,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22513,7 +22513,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22615,7 +22615,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22641,7 +22641,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -14.780000000000001 + "z": -14.779999999999998 }, "origin": "top" }, @@ -22667,7 +22667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22935,7 +22935,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -22960,7 +22960,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -22985,7 +22985,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23010,7 +23010,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23035,7 +23035,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23061,7 +23061,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23086,7 +23086,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23111,7 +23111,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23137,7 +23137,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23162,7 +23162,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23187,7 +23187,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23213,7 +23213,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23238,7 +23238,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23263,7 +23263,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23288,7 +23288,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -23312,7 +23312,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -23346,7 +23346,7 @@ "position": { "x": 50.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -23360,7 +23360,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -23617,7 +23617,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23642,7 +23642,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23667,7 +23667,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23692,7 +23692,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23717,7 +23717,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23743,7 +23743,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23768,7 +23768,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23793,7 +23793,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23819,7 +23819,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23844,7 +23844,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23869,7 +23869,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23895,7 +23895,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -23920,7 +23920,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23945,7 +23945,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -23970,7 +23970,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -23994,7 +23994,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24028,7 +24028,7 @@ "position": { "x": 59.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -24042,7 +24042,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24299,7 +24299,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24324,7 +24324,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24349,7 +24349,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24374,7 +24374,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24399,7 +24399,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24425,7 +24425,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24450,7 +24450,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24475,7 +24475,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24501,7 +24501,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24526,7 +24526,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24551,7 +24551,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24577,7 +24577,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -24602,7 +24602,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24627,7 +24627,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -9.780000000000001 + "z": -9.779999999999998 }, "origin": "top" }, @@ -24652,7 +24652,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 2.0 + "z": 2.000000000000007 }, "origin": "top" }, @@ -24676,7 +24676,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -24710,7 +24710,7 @@ "position": { "x": 68.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -24724,7 +24724,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -29887,7 +29887,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -29912,7 +29912,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -29937,7 +29937,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -29962,7 +29962,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -29987,7 +29987,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30012,7 +30012,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30037,7 +30037,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30062,7 +30062,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30087,7 +30087,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30112,7 +30112,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30137,7 +30137,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30162,7 +30162,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30187,7 +30187,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30212,7 +30212,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30237,7 +30237,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30262,7 +30262,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30288,7 +30288,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30313,7 +30313,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -30337,7 +30337,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30371,7 +30371,7 @@ "position": { "x": 50.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -30385,7 +30385,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30485,7 +30485,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30510,7 +30510,7 @@ "offset": { "x": 1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30535,7 +30535,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30560,7 +30560,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30585,7 +30585,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30610,7 +30610,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30635,7 +30635,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30660,7 +30660,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30685,7 +30685,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30710,7 +30710,7 @@ "offset": { "x": -1.0399999999999991, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30735,7 +30735,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30760,7 +30760,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30785,7 +30785,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30810,7 +30810,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -30835,7 +30835,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30860,7 +30860,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30886,7 +30886,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -30911,7 +30911,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -30935,7 +30935,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -30969,7 +30969,7 @@ "position": { "x": 59.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -30983,7 +30983,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -31083,7 +31083,7 @@ "offset": { "x": 1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31108,7 +31108,7 @@ "offset": { "x": 1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31133,7 +31133,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31158,7 +31158,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31183,7 +31183,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31208,7 +31208,7 @@ "offset": { "x": 0.0, "y": 1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31233,7 +31233,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31258,7 +31258,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31283,7 +31283,7 @@ "offset": { "x": -1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31308,7 +31308,7 @@ "offset": { "x": -1.0400000000000063, "y": 0.0, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31333,7 +31333,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31358,7 +31358,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31383,7 +31383,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31408,7 +31408,7 @@ "offset": { "x": 0.0, "y": -1.0400000000000063, - "z": -11.39 + "z": -11.389999999999997 }, "origin": "top" }, @@ -31433,7 +31433,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31458,7 +31458,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31484,7 +31484,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -13.780000000000001 + "z": -13.779999999999998 }, "origin": "top" }, @@ -31509,7 +31509,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": -7.390000000000001 + "z": -7.389999999999997 }, "origin": "top" }, @@ -31533,7 +31533,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, @@ -31567,7 +31567,7 @@ "position": { "x": 68.38, "y": 74.24, - "z": 34.650000000000006 + "z": 34.65 } }, "status": "succeeded" @@ -31581,7 +31581,7 @@ "offset": { "x": 0.0, "y": 0.0, - "z": 5.0 + "z": 5.000000000000007 }, "origin": "top" }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json index a79130779de9..3d18e932a567 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[0f71566d05][OT2_P20S_None_2_7_Walkthrough].json @@ -3293,7 +3293,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json index d974b6960585..ef9f55e77e7f 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[20cefcac62][OT2_P300M_P20S_TC_HS_TM_2_13_SmokeTestV3].json @@ -11889,7 +11889,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json index 0581fee89621..35ec253ed42b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[240b279ac3][OT2_P300S_Thermocycler_Moam_Error].json @@ -2680,7 +2680,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot 7 prevents thermocyclerModuleV1 from using slot 7.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_P300S_Thermocycler_Moam_Error.py\", line 19, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/legacy_protocol_core.py\", line 333, in load_module\n self._deck_layout[resolved_location] = geometry\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy/deck.py\", line 186, in __setitem__\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json index bf492fe07460..87642f0e06f4 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[25f79fd65e][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3].json @@ -96,6 +96,13 @@ }, { "commandType": "loadModule", + "error": { + "detail": "Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + }, "notes": [], "params": { "location": { @@ -103,448 +110,7 @@ }, "model": "temperatureModuleV2" }, - "result": { - "definition": { - "calibrationPoint": { - "x": 11.7, - "y": 8.75, - "z": 80.09 - }, - "compatibleWith": [ - "temperatureModuleV1" - ], - "dimensions": { - "bareOverallHeight": 84.0, - "overLabwareHeight": 0.0 - }, - "displayName": "Temperature Module GEN2", - "gripperOffsets": { - "default": { - "dropOffset": { - "x": 0.0, - "y": 0.0, - "z": 1.0 - }, - "pickUpOffset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - } - } - }, - "labwareOffset": { - "x": -1.45, - "y": -0.15, - "z": 80.09 - }, - "model": "temperatureModuleV2", - "moduleType": "temperatureModuleType", - "otSharedSchema": "module/schemas/2", - "quirks": [], - "slotTransforms": { - "ot2_short_trash": { - "3": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "6": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "9": { - "labwareOffset": [ - [ - -1, - -0.15, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - } - }, - "ot2_standard": { - "3": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "6": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - }, - "9": { - "labwareOffset": [ - [ - -1, - -0.3, - 0, - 0 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ] - ] - } - }, - "ot3_standard": { - "A1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "A3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "B1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "B3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "C1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "C3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "D1": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - }, - "D3": { - "labwareOffset": [ - [ - -71.09, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0, - 1 - ], - [ - 0, - 0, - 0.15, - 1 - ], - [ - 0, - 0, - 1, - 1.45 - ] - ] - } - } - } - }, - "model": "temperatureModuleV2" - }, - "status": "succeeded" + "status": "failed" } ], "config": { @@ -556,21 +122,25 @@ }, "errors": [ { - "detail": "DeckConflictError [line 17]: nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.", + "detail": "ProtocolCommandFailedError [line 17]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): IncompatibleAddressableAreaError: Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "opentrons.motion_planning.deck_conflict.DeckConflictError: nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.", + "detail": "IncompatibleAddressableAreaError: Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", "errorCode": "4000", - "errorInfo": { - "args": "('nest_1_reservoir_290ml in slot C4 prevents temperatureModuleV2 from using slot C3.',)", - "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol3.py\", line 17, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" - }, - "errorType": "PythonException", - "wrappedErrors": [] + "errorInfo": {}, + "errorType": "ProtocolCommandFailedError", + "wrappedErrors": [ + { + "detail": "Cannot use Temperature Module in C3, not compatible with one or more of the following fixtures: Slot C4", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + } + ] } ] } @@ -615,14 +185,7 @@ "author": "Derek Maggio ", "protocolName": "QA Protocol - Analysis Error - Module in Staging Area Column 3" }, - "modules": [ - { - "location": { - "slotName": "C3" - }, - "model": "temperatureModuleV2" - } - ], + "modules": [], "pipettes": [], "robotType": "OT-3 Standard", "runTimeParameters": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json index 58867a05b3fa..0693b30cc542 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[37c9086bf4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment_v4].json @@ -11809,7 +11809,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -11834,7 +11834,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -11860,7 +11860,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -11935,7 +11935,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -11960,7 +11960,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -11986,7 +11986,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -12061,7 +12061,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -12086,7 +12086,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -12112,7 +12112,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -12335,7 +12335,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -12360,7 +12360,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12385,7 +12385,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12410,7 +12410,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -12435,7 +12435,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12461,7 +12461,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12486,7 +12486,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12511,7 +12511,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12537,7 +12537,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12562,7 +12562,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -12587,7 +12587,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12613,7 +12613,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -12638,7 +12638,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -12663,7 +12663,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -12965,7 +12965,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -12990,7 +12990,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13015,7 +13015,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13040,7 +13040,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13065,7 +13065,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13091,7 +13091,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13116,7 +13116,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13141,7 +13141,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13167,7 +13167,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13192,7 +13192,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13217,7 +13217,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13243,7 +13243,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13268,7 +13268,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13293,7 +13293,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13595,7 +13595,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -13620,7 +13620,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13645,7 +13645,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13670,7 +13670,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13695,7 +13695,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13721,7 +13721,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13746,7 +13746,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13771,7 +13771,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13797,7 +13797,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -13822,7 +13822,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -13847,7 +13847,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13873,7 +13873,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -13898,7 +13898,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -13923,7 +13923,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -15118,7 +15118,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15144,7 +15144,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15220,7 +15220,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15246,7 +15246,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15322,7 +15322,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -15348,7 +15348,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16356,7 +16356,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16382,7 +16382,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16458,7 +16458,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16484,7 +16484,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16560,7 +16560,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -16586,7 +16586,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17594,7 +17594,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17620,7 +17620,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17696,7 +17696,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17722,7 +17722,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17798,7 +17798,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -17824,7 +17824,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18832,7 +18832,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18858,7 +18858,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18934,7 +18934,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -18960,7 +18960,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19036,7 +19036,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19062,7 +19062,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -19165,7 +19165,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19190,7 +19190,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19216,7 +19216,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -19291,7 +19291,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19316,7 +19316,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19342,7 +19342,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -19417,7 +19417,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -19442,7 +19442,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -19468,7 +19468,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -21541,7 +21541,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21667,7 +21667,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21793,7 +21793,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -21905,7 +21905,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -21930,7 +21930,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -21955,7 +21955,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -21981,7 +21981,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22083,7 +22083,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -22108,7 +22108,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22133,7 +22133,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22159,7 +22159,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22261,7 +22261,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -22286,7 +22286,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22311,7 +22311,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22337,7 +22337,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22479,7 +22479,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22581,7 +22581,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22683,7 +22683,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -22795,7 +22795,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22820,7 +22820,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -22845,7 +22845,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -22871,7 +22871,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -22973,7 +22973,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -22998,7 +22998,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -23023,7 +23023,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23049,7 +23049,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23151,7 +23151,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -23176,7 +23176,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -23201,7 +23201,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23227,7 +23227,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -23378,7 +23378,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23403,7 +23403,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23429,7 +23429,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23504,7 +23504,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23529,7 +23529,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23555,7 +23555,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23630,7 +23630,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -23655,7 +23655,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -23681,7 +23681,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -23845,7 +23845,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -23870,7 +23870,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -23895,7 +23895,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -23920,7 +23920,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -23945,7 +23945,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -23971,7 +23971,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -23996,7 +23996,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24021,7 +24021,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24047,7 +24047,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24072,7 +24072,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24097,7 +24097,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24123,7 +24123,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24148,7 +24148,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24173,7 +24173,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24423,7 +24423,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -24448,7 +24448,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24473,7 +24473,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24498,7 +24498,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24523,7 +24523,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24549,7 +24549,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24574,7 +24574,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24599,7 +24599,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -24625,7 +24625,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -24650,7 +24650,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -24675,7 +24675,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24701,7 +24701,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -24726,7 +24726,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -24751,7 +24751,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -25001,7 +25001,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -25026,7 +25026,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25051,7 +25051,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -25076,7 +25076,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -25101,7 +25101,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25127,7 +25127,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25152,7 +25152,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25177,7 +25177,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -25203,7 +25203,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -25228,7 +25228,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -25253,7 +25253,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25279,7 +25279,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -25304,7 +25304,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -25329,7 +25329,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -30796,7 +30796,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -30821,7 +30821,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -30896,7 +30896,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -30921,7 +30921,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -30996,7 +30996,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31021,7 +31021,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31096,7 +31096,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31121,7 +31121,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31147,7 +31147,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31394,7 +31394,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31419,7 +31419,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31494,7 +31494,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31519,7 +31519,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31594,7 +31594,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31619,7 +31619,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31694,7 +31694,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -31719,7 +31719,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31745,7 +31745,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -31992,7 +31992,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32017,7 +32017,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32092,7 +32092,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32117,7 +32117,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32192,7 +32192,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32217,7 +32217,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32292,7 +32292,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -32317,7 +32317,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32343,7 +32343,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -32613,7 +32613,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -32739,7 +32739,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -32865,7 +32865,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -33501,7 +33501,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33553,7 +33553,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33605,7 +33605,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33657,7 +33657,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33709,7 +33709,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33761,7 +33761,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33813,7 +33813,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33865,7 +33865,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33917,7 +33917,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -33969,7 +33969,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34021,7 +34021,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34073,7 +34073,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34125,7 +34125,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34177,7 +34177,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34229,7 +34229,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34281,7 +34281,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34333,7 +34333,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34385,7 +34385,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34437,7 +34437,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34489,7 +34489,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34541,7 +34541,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34593,7 +34593,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34645,7 +34645,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34697,7 +34697,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34749,7 +34749,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34801,7 +34801,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34853,7 +34853,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34905,7 +34905,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -34957,7 +34957,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35009,7 +35009,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35061,7 +35061,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35113,7 +35113,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35165,7 +35165,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35217,7 +35217,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35269,7 +35269,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -35321,7 +35321,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -36073,7 +36073,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36125,7 +36125,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36177,7 +36177,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -36255,7 +36255,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36307,7 +36307,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36359,7 +36359,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -36506,7 +36506,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36531,7 +36531,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36557,7 +36557,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -36632,7 +36632,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36657,7 +36657,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36683,7 +36683,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -36758,7 +36758,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -36783,7 +36783,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -36809,7 +36809,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -37032,7 +37032,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -37057,7 +37057,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37082,7 +37082,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37107,7 +37107,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37132,7 +37132,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37158,7 +37158,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37183,7 +37183,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37208,7 +37208,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37234,7 +37234,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37259,7 +37259,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37284,7 +37284,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37310,7 +37310,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37335,7 +37335,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37360,7 +37360,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37662,7 +37662,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -37687,7 +37687,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37712,7 +37712,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37737,7 +37737,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37762,7 +37762,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37788,7 +37788,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37813,7 +37813,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37838,7 +37838,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37864,7 +37864,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -37889,7 +37889,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -37914,7 +37914,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37940,7 +37940,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -37965,7 +37965,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -37990,7 +37990,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38292,7 +38292,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -38317,7 +38317,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38342,7 +38342,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38367,7 +38367,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -38392,7 +38392,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38418,7 +38418,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38443,7 +38443,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38468,7 +38468,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38494,7 +38494,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -38519,7 +38519,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -38544,7 +38544,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38570,7 +38570,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -38595,7 +38595,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -38620,7 +38620,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -39815,7 +39815,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39841,7 +39841,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39917,7 +39917,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -39943,7 +39943,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -40019,7 +40019,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -40045,7 +40045,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41053,7 +41053,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41079,7 +41079,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41155,7 +41155,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41181,7 +41181,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41257,7 +41257,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -41283,7 +41283,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42291,7 +42291,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42317,7 +42317,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42393,7 +42393,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42419,7 +42419,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42495,7 +42495,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -42521,7 +42521,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43529,7 +43529,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43555,7 +43555,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43631,7 +43631,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43657,7 +43657,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43733,7 +43733,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43759,7 +43759,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -43862,7 +43862,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -43887,7 +43887,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -43913,7 +43913,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -43988,7 +43988,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -44013,7 +44013,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -44039,7 +44039,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -44114,7 +44114,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -44139,7 +44139,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -44165,7 +44165,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -46238,7 +46238,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46364,7 +46364,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46490,7 +46490,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -46602,7 +46602,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46627,7 +46627,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -46652,7 +46652,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46678,7 +46678,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46780,7 +46780,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46805,7 +46805,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -46830,7 +46830,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46856,7 +46856,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -46958,7 +46958,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -46983,7 +46983,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47008,7 +47008,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47034,7 +47034,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47176,7 +47176,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47278,7 +47278,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47380,7 +47380,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -47492,7 +47492,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47517,7 +47517,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47542,7 +47542,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47568,7 +47568,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47670,7 +47670,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47695,7 +47695,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47720,7 +47720,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47746,7 +47746,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47848,7 +47848,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -47873,7 +47873,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -47898,7 +47898,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -47924,7 +47924,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -48075,7 +48075,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48100,7 +48100,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48126,7 +48126,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48201,7 +48201,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48226,7 +48226,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48252,7 +48252,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48327,7 +48327,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -48352,7 +48352,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -48378,7 +48378,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -48542,7 +48542,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -48567,7 +48567,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48592,7 +48592,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -48617,7 +48617,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -48642,7 +48642,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48668,7 +48668,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48693,7 +48693,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48718,7 +48718,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -48744,7 +48744,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -48769,7 +48769,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -48794,7 +48794,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48820,7 +48820,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -48845,7 +48845,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -48870,7 +48870,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49120,7 +49120,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -49145,7 +49145,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49170,7 +49170,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49195,7 +49195,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49220,7 +49220,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49246,7 +49246,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49271,7 +49271,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49296,7 +49296,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49322,7 +49322,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49347,7 +49347,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49372,7 +49372,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49398,7 +49398,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49423,7 +49423,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49448,7 +49448,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49698,7 +49698,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -49723,7 +49723,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49748,7 +49748,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49773,7 +49773,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49798,7 +49798,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49824,7 +49824,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49849,7 +49849,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -49874,7 +49874,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -49900,7 +49900,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -49925,7 +49925,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -49950,7 +49950,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -49976,7 +49976,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -50001,7 +50001,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -50026,7 +50026,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -55493,7 +55493,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55518,7 +55518,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55593,7 +55593,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55618,7 +55618,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55693,7 +55693,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55718,7 +55718,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55793,7 +55793,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -55818,7 +55818,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -55844,7 +55844,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56091,7 +56091,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56116,7 +56116,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56191,7 +56191,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56216,7 +56216,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56291,7 +56291,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56316,7 +56316,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56391,7 +56391,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56416,7 +56416,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56442,7 +56442,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56689,7 +56689,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56714,7 +56714,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56789,7 +56789,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56814,7 +56814,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56889,7 +56889,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -56914,7 +56914,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -56989,7 +56989,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -57014,7 +57014,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -57040,7 +57040,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -57310,7 +57310,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -57436,7 +57436,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -57562,7 +57562,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -58198,7 +58198,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58250,7 +58250,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58302,7 +58302,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58354,7 +58354,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58406,7 +58406,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58458,7 +58458,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58510,7 +58510,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58562,7 +58562,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58614,7 +58614,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58666,7 +58666,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58718,7 +58718,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58770,7 +58770,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58822,7 +58822,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58874,7 +58874,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58926,7 +58926,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -58978,7 +58978,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59030,7 +59030,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59082,7 +59082,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59134,7 +59134,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59186,7 +59186,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59238,7 +59238,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59290,7 +59290,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59342,7 +59342,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59394,7 +59394,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59446,7 +59446,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59498,7 +59498,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59550,7 +59550,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59602,7 +59602,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59654,7 +59654,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59706,7 +59706,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59758,7 +59758,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59810,7 +59810,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59862,7 +59862,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59914,7 +59914,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -59966,7 +59966,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -60018,7 +60018,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -60770,7 +60770,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60822,7 +60822,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60874,7 +60874,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -60952,7 +60952,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61004,7 +61004,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61056,7 +61056,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -61203,7 +61203,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61228,7 +61228,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61254,7 +61254,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61329,7 +61329,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61354,7 +61354,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61380,7 +61380,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61455,7 +61455,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -61480,7 +61480,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -61506,7 +61506,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -61729,7 +61729,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -61754,7 +61754,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -61779,7 +61779,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61804,7 +61804,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -61829,7 +61829,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -61855,7 +61855,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -61880,7 +61880,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -61905,7 +61905,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61931,7 +61931,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -61956,7 +61956,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -61981,7 +61981,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62007,7 +62007,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62032,7 +62032,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62057,7 +62057,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62359,7 +62359,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -62384,7 +62384,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62409,7 +62409,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62434,7 +62434,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -62459,7 +62459,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62485,7 +62485,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62510,7 +62510,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62535,7 +62535,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62561,7 +62561,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62586,7 +62586,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -62611,7 +62611,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62637,7 +62637,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -62662,7 +62662,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -62687,7 +62687,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -62989,7 +62989,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -63014,7 +63014,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63039,7 +63039,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63064,7 +63064,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -63089,7 +63089,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63115,7 +63115,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63140,7 +63140,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63165,7 +63165,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63191,7 +63191,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -63216,7 +63216,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -63241,7 +63241,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63267,7 +63267,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -63292,7 +63292,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -63317,7 +63317,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -64512,7 +64512,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64538,7 +64538,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64614,7 +64614,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64640,7 +64640,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64716,7 +64716,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -64742,7 +64742,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65750,7 +65750,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65776,7 +65776,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65852,7 +65852,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65878,7 +65878,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65954,7 +65954,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -65980,7 +65980,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -66988,7 +66988,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67014,7 +67014,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67090,7 +67090,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67116,7 +67116,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67192,7 +67192,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -67218,7 +67218,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68226,7 +68226,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68252,7 +68252,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68328,7 +68328,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68354,7 +68354,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68430,7 +68430,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68456,7 +68456,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -68559,7 +68559,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68584,7 +68584,7 @@ "position": { "x": 14.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68610,7 +68610,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -68685,7 +68685,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68710,7 +68710,7 @@ "position": { "x": 23.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68736,7 +68736,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -68811,7 +68811,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -68836,7 +68836,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -68862,7 +68862,7 @@ "position": { "x": 59.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -70935,7 +70935,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71061,7 +71061,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71187,7 +71187,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -71299,7 +71299,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71324,7 +71324,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71349,7 +71349,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71375,7 +71375,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71477,7 +71477,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71502,7 +71502,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71527,7 +71527,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71553,7 +71553,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71655,7 +71655,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -71680,7 +71680,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -71705,7 +71705,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71731,7 +71731,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -71873,7 +71873,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -71975,7 +71975,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -72077,7 +72077,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -72189,7 +72189,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72214,7 +72214,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72239,7 +72239,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72265,7 +72265,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72367,7 +72367,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72392,7 +72392,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72417,7 +72417,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72443,7 +72443,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72545,7 +72545,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -72570,7 +72570,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -72595,7 +72595,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72621,7 +72621,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -72772,7 +72772,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -72797,7 +72797,7 @@ "position": { "x": 21.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -72823,7 +72823,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -72898,7 +72898,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -72923,7 +72923,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -72949,7 +72949,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -73024,7 +73024,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -73049,7 +73049,7 @@ "position": { "x": 39.375, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -73075,7 +73075,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -73239,7 +73239,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -73264,7 +73264,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73289,7 +73289,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73314,7 +73314,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73339,7 +73339,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73365,7 +73365,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73390,7 +73390,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73415,7 +73415,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -73441,7 +73441,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73466,7 +73466,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73491,7 +73491,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73517,7 +73517,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73542,7 +73542,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73567,7 +73567,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -73817,7 +73817,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -73842,7 +73842,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73867,7 +73867,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -73892,7 +73892,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -73917,7 +73917,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73943,7 +73943,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -73968,7 +73968,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -73993,7 +73993,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74019,7 +74019,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74044,7 +74044,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74069,7 +74069,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74095,7 +74095,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74120,7 +74120,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74145,7 +74145,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74395,7 +74395,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -74420,7 +74420,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74445,7 +74445,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74470,7 +74470,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74495,7 +74495,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74521,7 +74521,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74546,7 +74546,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74571,7 +74571,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -74597,7 +74597,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -74622,7 +74622,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -74647,7 +74647,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74673,7 +74673,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -74698,7 +74698,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -74723,7 +74723,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -80190,7 +80190,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80215,7 +80215,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80290,7 +80290,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80315,7 +80315,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80390,7 +80390,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80415,7 +80415,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80490,7 +80490,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80515,7 +80515,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80541,7 +80541,7 @@ "position": { "x": 68.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80788,7 +80788,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80813,7 +80813,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80888,7 +80888,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -80913,7 +80913,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -80988,7 +80988,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81013,7 +81013,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81088,7 +81088,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81113,7 +81113,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81139,7 +81139,7 @@ "position": { "x": 77.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81386,7 +81386,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81411,7 +81411,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81486,7 +81486,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81511,7 +81511,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81586,7 +81586,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81611,7 +81611,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81686,7 +81686,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -81711,7 +81711,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -81737,7 +81737,7 @@ "position": { "x": 86.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -82007,7 +82007,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82133,7 +82133,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82259,7 +82259,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 31.0 }, @@ -82895,7 +82895,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -82947,7 +82947,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -82999,7 +82999,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83051,7 +83051,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83103,7 +83103,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83155,7 +83155,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83207,7 +83207,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83259,7 +83259,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83311,7 +83311,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83363,7 +83363,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83415,7 +83415,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83467,7 +83467,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83519,7 +83519,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83571,7 +83571,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83623,7 +83623,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83675,7 +83675,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83727,7 +83727,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83779,7 +83779,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83831,7 +83831,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83883,7 +83883,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83935,7 +83935,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -83987,7 +83987,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84039,7 +84039,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84091,7 +84091,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84143,7 +84143,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84195,7 +84195,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84247,7 +84247,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84299,7 +84299,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84351,7 +84351,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84403,7 +84403,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84455,7 +84455,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84507,7 +84507,7 @@ "position": { "x": 95.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84559,7 +84559,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84611,7 +84611,7 @@ "position": { "x": 104.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84663,7 +84663,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -84715,7 +84715,7 @@ "position": { "x": 113.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -85467,7 +85467,7 @@ "position": { "x": 48.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85519,7 +85519,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85571,7 +85571,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 32.0 }, @@ -85649,7 +85649,7 @@ "position": { "x": -5.624999999999998, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -85701,7 +85701,7 @@ "position": { "x": 3.375, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, @@ -85753,7 +85753,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 30.0 }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json index 6a28756037cf..9ccf2c716e09 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[3b1bfd0d2d][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 9.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin2.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 425, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json index 1c888cd46cc7..636e3ae1cbc3 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4835239037][OT2_P300M_P20S_MM_TM_TC1_5_2_6_PD40Error].json @@ -6924,7 +6924,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/legacy_commands/publisher.py\", line 113, in publish_context\n yield\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] @@ -6965,7 +6965,7 @@ "errorInfo": { "args": "('Cannot aspirate more than pipette max volume',)", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 63, in run_protocol\n execute_json_v4.dispatch_json(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v4.py\", line 272, in dispatch_json\n pipette_command_map[command_type]( # type: ignore\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_json_v3.py\", line 159, in _aspirate\n pipette.aspirate(volume, location)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/instrument_context.py\", line 270, in aspirate\n self._core.aspirate(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py\", line 119, in aspirate\n new_volume <= self._pipette_dict[\"working_volume\"]\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json index 5bd1dec9c82e..babe140d8306 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[4e17da0b57][Flex_P1000_96_Gripper_TC_TM_HS_AnalysisError_GripperCollisionWithTips].json @@ -12252,316 +12252,6 @@ "strategy": "usingGripper" }, "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "temperatureModule/deactivate", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": {}, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/closeLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/setTargetTemperature", - "params": { - "celsius": 40.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "params": { - "rpm": 1000.0 - }, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "B2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "waitForDuration", - "params": { - "message": "", - "seconds": 60.0 - }, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/openLabwareLatch", - "params": {}, - "status": "failed" - }, - { - "commandType": "moveLabware", - "params": { - "newLocation": { - "slotName": "D2" - }, - "strategy": "usingGripper" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 150.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dispenseInPlace", - "params": { - "flowRate": 6.0, - "volume": 50.0 - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" - }, - { - "commandType": "pickUpTip", - "params": { - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "top" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "aspirate", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 1.0 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 6.0, - "volume": 50.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "A1" - }, - "status": "failed" - }, - { - "commandType": "moveToAddressableArea", - "params": { - "addressableAreaName": "movableTrashA3", - "forceDirect": false, - "offset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "stayAtHighestPossibleZ": false - }, - "status": "failed" - }, - { - "commandType": "dropTipInPlace", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json index 3fcada170015..ce2f5357e411 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[512a897a47][Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Cannot load a module onto a staging slot.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 808, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 812, in load_module\n raise ValueError(\"Cannot load a module onto a staging slot.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json index 6dfd0ab19b6d..b36a44a54574 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[52a42597a5][OT2_P300M_P20S_MM_HS_TD_TC_6_1_AllMods_Error].json @@ -7280,31 +7280,6 @@ "notes": [], "params": {}, "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateHeater", - "params": {}, - "status": "failed" - }, - { - "commandType": "heaterShaker/deactivateShaker", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/openLid", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateBlock", - "params": {}, - "status": "failed" - }, - { - "commandType": "thermocycler/deactivateLid", - "params": {}, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json index 9ac5392e5ffb..c43a9f80c613 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5931902632][Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict].json @@ -137,7 +137,7 @@ "errorInfo": { "args": "('thermocyclerModuleV2 in slot B1 prevents trash bin from using slot A1.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 514, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 529, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 148, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TC_2_16_AnalysisError_TrashBinAndThermocyclerConflict.py\", line 13, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 518, in load_trash_bin\n trash_bin = self._core.load_trash_bin(slot_name, addressable_area_name)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 530, in load_trash_bin\n self._add_disposal_location_to_engine(trash_bin)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 149, in _add_disposal_location_to_engine\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 210, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json index 3e3b00c26a8f..a5d379ba794c 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[5fc4f3adbc][OT2_P20SRight_None_6_1_SimpleTransferError].json @@ -1718,39 +1718,6 @@ "wellName": "A1" }, "status": "failed" - }, - { - "commandType": "dispense", - "params": { - "flowRate": 3.78, - "volume": 10.0, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0.5 - }, - "origin": "bottom" - }, - "wellName": "B1" - }, - "status": "failed" - }, - { - "commandType": "dropTip", - "params": { - "alternateDropLocation": false, - "wellLocation": { - "offset": { - "x": 0, - "y": 0, - "z": 0 - }, - "origin": "default" - }, - "wellName": "A1" - }, - "status": "failed" } ], "config": { diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json index aab8caadd155..af560dfb9f33 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[753ac8811f][OT2_None_None_2_13_PythonSyntaxError].json @@ -31,7 +31,7 @@ "msg": "No module named 'superspecialmagic'", "name": "superspecialmagic", "path": "None", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 218, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 84, in _run\n await self._run_func()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/task_queue.py\", line 61, in _do_run\n await func(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/protocol_runner.py\", line 219, in run_func\n await self._legacy_executor.execute(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_wrappers.py\", line 180, in execute\n await to_thread.run_sync(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/to_thread.py\", line 33, in run_sync\n return await get_asynclib().run_sync_in_worker_thread(\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 877, in run_sync_in_worker_thread\n return await future\n\n File \"/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py\", line 807, in run\n result = context.run(func, *args)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute.py\", line 40, in run_protocol\n run_python(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 95, in run_python\n exec(proto.contents, new_globs)\n\n File \"OT2_None_None_2_13_PythonSyntaxError.py\", line 4, in \n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json index 031b3816aa95..89c43a035a9b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7be98bf838][Flex_None_None_2_16_AnalysisError_TrashBinInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Invalid location for trash bin: C2.\\nValid slots: Any slot in column 1 or 3.',)", "class": "InvalidTrashBinLocationError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 509, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 327, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 513, in load_trash_bin\n addressable_area_name = validation.ensure_and_convert_trash_bin_location(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/validation.py\", line 331, in ensure_and_convert_trash_bin_location\n raise InvalidTrashBinLocationError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json index bcdd4fb2a958..54003322788b 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[7ea2fdcec4][Flex_P1000MLeft_P50MRight_HS_MM_TC_TM_2_15_ABR3_Illumina_DNA_Enrichment].json @@ -9597,7 +9597,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 50.0 }, @@ -9709,7 +9709,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -9821,7 +9821,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 10.0 }, @@ -9846,7 +9846,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -9871,7 +9871,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 90.0 }, @@ -9897,7 +9897,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 90.0 }, @@ -10033,7 +10033,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10085,7 +10085,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10137,7 +10137,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10189,7 +10189,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 121.0 }, @@ -10274,7 +10274,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -10299,7 +10299,7 @@ "position": { "x": 12.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 101.0 }, @@ -10325,7 +10325,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 101.0 }, @@ -10808,7 +10808,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 125.0 }, @@ -10833,7 +10833,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -10858,7 +10858,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -10883,7 +10883,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -10908,7 +10908,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -10934,7 +10934,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -10959,7 +10959,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -10984,7 +10984,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11010,7 +11010,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11035,7 +11035,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -11060,7 +11060,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -11086,7 +11086,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 80.0 }, @@ -11111,7 +11111,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -11136,7 +11136,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 100.0 }, @@ -11717,7 +11717,7 @@ "position": { "x": 57.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -11743,7 +11743,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -12222,7 +12222,7 @@ "position": { "x": 66.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -12248,7 +12248,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -12727,7 +12727,7 @@ "position": { "x": 75.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -12753,7 +12753,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -13232,7 +13232,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 200.0 }, @@ -13258,7 +13258,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 21.950000000000003 + "z": 21.95 }, "volume": 200.0 }, @@ -13361,7 +13361,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 } }, "status": "succeeded" @@ -13386,7 +13386,7 @@ "position": { "x": 32.3, "y": 74.15, - "z": 22.200000000000003 + "z": 22.2 }, "volume": 200.0 }, @@ -13412,7 +13412,7 @@ "position": { "x": 41.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 200.0 }, @@ -14336,7 +14336,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 2.079999999999993 + "z": 2.08 }, "volume": 22.0 }, @@ -14448,7 +14448,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 4.0 }, @@ -14473,7 +14473,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -14498,7 +14498,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14524,7 +14524,7 @@ "position": { "x": 84.375, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14666,7 +14666,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 5.0 }, @@ -14778,7 +14778,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 20.0 }, @@ -14803,7 +14803,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 } }, "status": "succeeded" @@ -14828,7 +14828,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -14854,7 +14854,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.079999999999993 + "z": 1.08 }, "volume": 45.0 }, @@ -15012,7 +15012,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 } }, "status": "succeeded" @@ -15037,7 +15037,7 @@ "position": { "x": 30.375000000000004, "y": 356.2, - "z": 1.329999999999993 + "z": 1.33 }, "volume": 46.0 }, @@ -15063,7 +15063,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 46.0 }, @@ -15227,7 +15227,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 40.5 }, @@ -15252,7 +15252,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15277,7 +15277,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -15302,7 +15302,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -15327,7 +15327,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15353,7 +15353,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15378,7 +15378,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15403,7 +15403,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -15429,7 +15429,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 60.0 }, @@ -15454,7 +15454,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -15479,7 +15479,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15505,7 +15505,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 60.0 }, @@ -15530,7 +15530,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 } }, "status": "succeeded" @@ -15555,7 +15555,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 26.950000000000003 + "z": 26.95 }, "volume": 30.0 }, @@ -17654,7 +17654,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17679,7 +17679,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17754,7 +17754,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17779,7 +17779,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17854,7 +17854,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17879,7 +17879,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -17954,7 +17954,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 } }, "status": "succeeded" @@ -17979,7 +17979,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, @@ -18005,7 +18005,7 @@ "position": { "x": 50.3, "y": 74.15, - "z": 22.950000000000003 + "z": 22.95 }, "volume": 32.0 }, diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json index 02165d003c73..d27e70c456fc 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[8df082e960][OT2_P300MLeft_MM_TM_2_4_Zymo].json @@ -10913,7 +10913,7 @@ "errorInfo": { "args": "()", "class": "AssertionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 442, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 204, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 243, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/protocol_engine.py\", line 512, in finish\n await exit_stack.aclose()\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 656, in aclose\n await self.__aexit__(None, None, None)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 714, in __aexit__\n raise exc_details[1]\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 697, in __aexit__\n cb_suppress = await cb(*exc_details)\n\n File \"/usr/local/lib/python3.10/contextlib.py\", line 608, in _exit_wrapper\n await callback(*args, **kwds)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 102, in stop\n await p.teardown()\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 112, in teardown\n await self._action_dispatching_task\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_runner/legacy_context_plugin.py\", line 160, in _dispatch_all_actions\n self.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/plugins.py\", line 37, in dispatch\n return self._action_dispatcher.dispatch(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/actions/action_dispatcher.py\", line 30, in dispatch\n self._sink.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/state.py\", line 209, in handle_action\n substore.handle_action(action)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/commands.py\", line 261, in handle_action\n self._state.command_history.set_command_running(running_command)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_engine/state/command_history.py\", line 175, in set_command_running\n assert self.get_running_command() is None\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json index ba5644090a29..4beea85705a8 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ac35bb394d][Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Staging areas not permitted for trash bin.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 508, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_TrashBinInStagingAreaCol4.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 512, in load_trash_bin\n raise ValueError(\"Staging areas not permitted for trash bin.\")\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json index fbcb54a5e134..baba4ad26faa 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[cda954ef1e][Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol].json @@ -17,18 +17,18 @@ }, "errors": [ { - "detail": "ValueError [line 15]: A magneticModuleType cannot be loaded into slot C1", + "detail": "ValueError [line 15]: Module Type magneticModuleType does not have a related fixture ID.", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "ValueError: A magneticModuleType cannot be loaded into slot C1", + "detail": "ValueError: Module Type magneticModuleType does not have a related fixture ID.", "errorCode": "4000", "errorInfo": { - "args": "('A magneticModuleType cannot be loaded into slot C1',)", + "args": "('Module Type magneticModuleType does not have a related fixture ID.',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_MM_2_16_AnalysisError_MagneticModuleInFlexProtocol.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 413, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 637, in _ensure_module_location\n cutout_fixture_id = ModuleType.to_module_fixture_id(module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/hardware_control/modules/types.py\", line 79, in to_module_fixture_id\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json index e1ab5bc62474..515359e16722 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[ce0f35b3c6][Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('A temperatureModuleType cannot be loaded into slot C2',)", "class": "ValueError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 412, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 631, in _ensure_module_location\n raise ValueError(f\"A {module_type.value} cannot be loaded into slot {slot}\")\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_TM_2_16_AnalysisError_ModuleInCol2.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 413, in load_module\n self._ensure_module_location(normalized_deck_slot, module_type)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 646, in _ensure_module_location\n raise ValueError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json index 85e9ca095c0b..64864e5d7151 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[d8ec3534d4][Flex_P1000_96_TM_2_16_AnalysisError_ModuleAndWasteChuteConflict].json @@ -1219,6 +1219,24 @@ } }, "status": "succeeded" + }, + { + "commandType": "loadModule", + "error": { + "detail": "Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + }, + "notes": [], + "params": { + "location": { + "slotName": "D3" + }, + "model": "temperatureModuleV2" + }, + "status": "failed" } ], "config": { @@ -1230,17 +1248,25 @@ }, "errors": [ { - "detail": "IncompatibleAddressableAreaError [line 19]: Error 4000 GENERAL_ERROR (IncompatibleAddressableAreaError): Cannot use Slot D3, not compatible with one or more of the following fixtures: Waste Chute", + "detail": "ProtocolCommandFailedError [line 19]: Error 4000 GENERAL_ERROR (ProtocolCommandFailedError): IncompatibleAddressableAreaError: Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", "errorCode": "4000", "errorInfo": {}, "errorType": "ExceptionInProtocolError", "wrappedErrors": [ { - "detail": "Cannot use Slot D3, not compatible with one or more of the following fixtures: Waste Chute", + "detail": "IncompatibleAddressableAreaError: Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", "errorCode": "4000", "errorInfo": {}, - "errorType": "IncompatibleAddressableAreaError", - "wrappedErrors": [] + "errorType": "ProtocolCommandFailedError", + "wrappedErrors": [ + { + "detail": "Cannot use Temperature Module in D3, not compatible with one or more of the following fixtures: Waste Chute", + "errorCode": "4000", + "errorInfo": {}, + "errorType": "IncompatibleAddressableAreaError", + "wrappedErrors": [] + } + ] } ] } diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json index 57381580c07d..257a29f5a73e 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[dc8ac87114][Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp].json @@ -28,7 +28,7 @@ "errorInfo": { "args": "('Fixed Trash is not supported on Flex protocols in API Version 2.16 and above.',)", "class": "APIVersionError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1114, in fixed_trash\n raise APIVersionError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"Flex_None_None_2_16_AnalysisError_AccessToFixedTrashProp.py\", line 15, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 1118, in fixed_trash\n raise APIVersionError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json index 4cf6892135d5..a54c74b3347a 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[e49dae5293][OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1].json @@ -478,7 +478,7 @@ "errorInfo": { "args": "('trash bin in slot 12 prevents heaterShakerModuleV1 from using slot 11.',)", "class": "DeckConflictError", - "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 810, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 424, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" + "traceback": " File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/execution/execute_python.py\", line 124, in run_python\n exec(\"run(__context)\", new_globs)\n\n File \"\", line 1, in \n\n File \"OT2_None_None_HS_2_16_AnalysisError_HeaterShakerConflictWithTrashBin1.py\", line 11, in run\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocols/api_support/util.py\", line 383, in _check_version_wrapper\n return decorated_obj(*args, **kwargs)\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/protocol_context.py\", line 814, in load_module\n module_core = self._core.load_module(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/protocol.py\", line 425, in load_module\n deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/protocol_api/core/engine/deck_conflict.py\", line 203, in check\n wrapped_deck_conflict.check(\n\n File \"/usr/local/lib/python3.10/site-packages/opentrons/motion_planning/deck_conflict.py\", line 223, in check\n raise DeckConflictError(\n" }, "errorType": "PythonException", "wrappedErrors": [] diff --git a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json index 1912a8a3d55e..0245a572ca92 100644 --- a/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json +++ b/app-testing/tests/__snapshots__/analyses_snapshot_test/test_analysis_snapshot[f91ecb541c][OT2_P300M_P20S_TC_HS_TM_2_17_SmokeTestV3].json @@ -6,12 +6,15543 @@ "params": {}, "result": {}, "status": "succeeded" + }, + { + "commandType": "setRailLights", + "notes": [], + "params": { + "on": true + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Let there be light! True 🌠🌠🌠", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is the door is closed? True 🚪🚪🚪", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Is this a simulation? True 🔮🔮🔮", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "custom", + "notes": [], + "params": { + "legacyCommandText": "Running against API Version: 2.17", + "legacyCommandType": "command.COMMENT" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-300ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.49 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 300 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_300ul", + "tipLength": 59.3, + "tipOverlap": 7.47 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 74.24, + "z": 5.39 + }, + "A10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 74.24, + "z": 5.39 + }, + "A11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 74.24, + "z": 5.39 + }, + "A12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 74.24, + "z": 5.39 + }, + "A2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 74.24, + "z": 5.39 + }, + "A3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 74.24, + "z": 5.39 + }, + "A4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 74.24, + "z": 5.39 + }, + "A5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 74.24, + "z": 5.39 + }, + "A6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 74.24, + "z": 5.39 + }, + "A7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 74.24, + "z": 5.39 + }, + "A8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 74.24, + "z": 5.39 + }, + "A9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 74.24, + "z": 5.39 + }, + "B1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 65.24, + "z": 5.39 + }, + "B10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 65.24, + "z": 5.39 + }, + "B11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 65.24, + "z": 5.39 + }, + "B12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 65.24, + "z": 5.39 + }, + "B2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 65.24, + "z": 5.39 + }, + "B3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 65.24, + "z": 5.39 + }, + "B4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 65.24, + "z": 5.39 + }, + "B5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 65.24, + "z": 5.39 + }, + "B6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 65.24, + "z": 5.39 + }, + "B7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 65.24, + "z": 5.39 + }, + "B8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 65.24, + "z": 5.39 + }, + "B9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 65.24, + "z": 5.39 + }, + "C1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 56.24, + "z": 5.39 + }, + "C10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 56.24, + "z": 5.39 + }, + "C11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 56.24, + "z": 5.39 + }, + "C12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 56.24, + "z": 5.39 + }, + "C2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 56.24, + "z": 5.39 + }, + "C3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 56.24, + "z": 5.39 + }, + "C4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 56.24, + "z": 5.39 + }, + "C5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 56.24, + "z": 5.39 + }, + "C6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 56.24, + "z": 5.39 + }, + "C7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 56.24, + "z": 5.39 + }, + "C8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 56.24, + "z": 5.39 + }, + "C9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 56.24, + "z": 5.39 + }, + "D1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 47.24, + "z": 5.39 + }, + "D10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 47.24, + "z": 5.39 + }, + "D11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 47.24, + "z": 5.39 + }, + "D12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 47.24, + "z": 5.39 + }, + "D2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 47.24, + "z": 5.39 + }, + "D3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 47.24, + "z": 5.39 + }, + "D4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 47.24, + "z": 5.39 + }, + "D5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 47.24, + "z": 5.39 + }, + "D6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 47.24, + "z": 5.39 + }, + "D7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 47.24, + "z": 5.39 + }, + "D8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 47.24, + "z": 5.39 + }, + "D9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 47.24, + "z": 5.39 + }, + "E1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 38.24, + "z": 5.39 + }, + "E10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 38.24, + "z": 5.39 + }, + "E11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 38.24, + "z": 5.39 + }, + "E12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 38.24, + "z": 5.39 + }, + "E2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 38.24, + "z": 5.39 + }, + "E3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 38.24, + "z": 5.39 + }, + "E4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 38.24, + "z": 5.39 + }, + "E5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 38.24, + "z": 5.39 + }, + "E6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 38.24, + "z": 5.39 + }, + "E7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 38.24, + "z": 5.39 + }, + "E8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 38.24, + "z": 5.39 + }, + "E9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 38.24, + "z": 5.39 + }, + "F1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 29.24, + "z": 5.39 + }, + "F10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 29.24, + "z": 5.39 + }, + "F11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 29.24, + "z": 5.39 + }, + "F12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 29.24, + "z": 5.39 + }, + "F2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 29.24, + "z": 5.39 + }, + "F3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 29.24, + "z": 5.39 + }, + "F4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 29.24, + "z": 5.39 + }, + "F5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 29.24, + "z": 5.39 + }, + "F6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 29.24, + "z": 5.39 + }, + "F7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 29.24, + "z": 5.39 + }, + "F8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 29.24, + "z": 5.39 + }, + "F9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 29.24, + "z": 5.39 + }, + "G1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 20.24, + "z": 5.39 + }, + "G10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 20.24, + "z": 5.39 + }, + "G11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 20.24, + "z": 5.39 + }, + "G12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 20.24, + "z": 5.39 + }, + "G2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 20.24, + "z": 5.39 + }, + "G3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 20.24, + "z": 5.39 + }, + "G4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 20.24, + "z": 5.39 + }, + "G5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 20.24, + "z": 5.39 + }, + "G6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 20.24, + "z": 5.39 + }, + "G7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 20.24, + "z": 5.39 + }, + "G8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 20.24, + "z": 5.39 + }, + "G9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 20.24, + "z": 5.39 + }, + "H1": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 14.38, + "y": 11.24, + "z": 5.39 + }, + "H10": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 95.38, + "y": 11.24, + "z": 5.39 + }, + "H11": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 104.38, + "y": 11.24, + "z": 5.39 + }, + "H12": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 113.38, + "y": 11.24, + "z": 5.39 + }, + "H2": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 23.38, + "y": 11.24, + "z": 5.39 + }, + "H3": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 32.38, + "y": 11.24, + "z": 5.39 + }, + "H4": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 41.38, + "y": 11.24, + "z": 5.39 + }, + "H5": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 50.38, + "y": 11.24, + "z": 5.39 + }, + "H6": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 59.38, + "y": 11.24, + "z": 5.39 + }, + "H7": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 68.38, + "y": 11.24, + "z": 5.39 + }, + "H8": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 77.38, + "y": 11.24, + "z": 5.39 + }, + "H9": { + "depth": 59.3, + "diameter": 5.23, + "shape": "circular", + "totalLiquidVolume": 300, + "x": 86.38, + "y": 11.24, + "z": 5.39 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "Opentrons", + "brandId": [], + "links": [ + "https://shop.opentrons.com/collections/opentrons-tips/products/opentrons-10ul-tips" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 64.69 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "tipRack", + "displayName": "Opentrons OT-2 96 Tip Rack 20 µL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "opentrons_96_tiprack_20ul", + "tipLength": 39.2, + "tipOverlap": 8.25 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 74.24, + "z": 25.49 + }, + "A10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 74.24, + "z": 25.49 + }, + "A11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 74.24, + "z": 25.49 + }, + "A12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 74.24, + "z": 25.49 + }, + "A2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 74.24, + "z": 25.49 + }, + "A3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 74.24, + "z": 25.49 + }, + "A4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 74.24, + "z": 25.49 + }, + "A5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 74.24, + "z": 25.49 + }, + "A6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 74.24, + "z": 25.49 + }, + "A7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 74.24, + "z": 25.49 + }, + "A8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 74.24, + "z": 25.49 + }, + "A9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 74.24, + "z": 25.49 + }, + "B1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 65.24, + "z": 25.49 + }, + "B10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 65.24, + "z": 25.49 + }, + "B11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 65.24, + "z": 25.49 + }, + "B12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 65.24, + "z": 25.49 + }, + "B2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 65.24, + "z": 25.49 + }, + "B3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 65.24, + "z": 25.49 + }, + "B4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 65.24, + "z": 25.49 + }, + "B5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 65.24, + "z": 25.49 + }, + "B6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 65.24, + "z": 25.49 + }, + "B7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 65.24, + "z": 25.49 + }, + "B8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 65.24, + "z": 25.49 + }, + "B9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 65.24, + "z": 25.49 + }, + "C1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 56.24, + "z": 25.49 + }, + "C10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 56.24, + "z": 25.49 + }, + "C11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 56.24, + "z": 25.49 + }, + "C12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 56.24, + "z": 25.49 + }, + "C2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 56.24, + "z": 25.49 + }, + "C3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 56.24, + "z": 25.49 + }, + "C4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 56.24, + "z": 25.49 + }, + "C5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 56.24, + "z": 25.49 + }, + "C6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 56.24, + "z": 25.49 + }, + "C7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 56.24, + "z": 25.49 + }, + "C8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 56.24, + "z": 25.49 + }, + "C9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 56.24, + "z": 25.49 + }, + "D1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 47.24, + "z": 25.49 + }, + "D10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 47.24, + "z": 25.49 + }, + "D11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 47.24, + "z": 25.49 + }, + "D12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 47.24, + "z": 25.49 + }, + "D2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 47.24, + "z": 25.49 + }, + "D3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 47.24, + "z": 25.49 + }, + "D4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 47.24, + "z": 25.49 + }, + "D5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 47.24, + "z": 25.49 + }, + "D6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 47.24, + "z": 25.49 + }, + "D7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 47.24, + "z": 25.49 + }, + "D8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 47.24, + "z": 25.49 + }, + "D9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 47.24, + "z": 25.49 + }, + "E1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 38.24, + "z": 25.49 + }, + "E10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 38.24, + "z": 25.49 + }, + "E11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 38.24, + "z": 25.49 + }, + "E12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 38.24, + "z": 25.49 + }, + "E2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 38.24, + "z": 25.49 + }, + "E3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 38.24, + "z": 25.49 + }, + "E4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 38.24, + "z": 25.49 + }, + "E5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 38.24, + "z": 25.49 + }, + "E6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 38.24, + "z": 25.49 + }, + "E7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 38.24, + "z": 25.49 + }, + "E8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 38.24, + "z": 25.49 + }, + "E9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 38.24, + "z": 25.49 + }, + "F1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 29.24, + "z": 25.49 + }, + "F10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 29.24, + "z": 25.49 + }, + "F11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 29.24, + "z": 25.49 + }, + "F12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 29.24, + "z": 25.49 + }, + "F2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 29.24, + "z": 25.49 + }, + "F3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 29.24, + "z": 25.49 + }, + "F4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 29.24, + "z": 25.49 + }, + "F5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 29.24, + "z": 25.49 + }, + "F6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 29.24, + "z": 25.49 + }, + "F7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 29.24, + "z": 25.49 + }, + "F8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 29.24, + "z": 25.49 + }, + "F9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 29.24, + "z": 25.49 + }, + "G1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 20.24, + "z": 25.49 + }, + "G10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 20.24, + "z": 25.49 + }, + "G11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 20.24, + "z": 25.49 + }, + "G12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 20.24, + "z": 25.49 + }, + "G2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 20.24, + "z": 25.49 + }, + "G3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 20.24, + "z": 25.49 + }, + "G4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 20.24, + "z": 25.49 + }, + "G5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 20.24, + "z": 25.49 + }, + "G6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 20.24, + "z": 25.49 + }, + "G7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 20.24, + "z": 25.49 + }, + "G8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 20.24, + "z": 25.49 + }, + "G9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 20.24, + "z": 25.49 + }, + "H1": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 14.38, + "y": 11.24, + "z": 25.49 + }, + "H10": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 95.38, + "y": 11.24, + "z": 25.49 + }, + "H11": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 104.38, + "y": 11.24, + "z": 25.49 + }, + "H12": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 113.38, + "y": 11.24, + "z": 25.49 + }, + "H2": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 23.38, + "y": 11.24, + "z": 25.49 + }, + "H3": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 32.38, + "y": 11.24, + "z": 25.49 + }, + "H4": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 41.38, + "y": 11.24, + "z": 25.49 + }, + "H5": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 50.38, + "y": 11.24, + "z": 25.49 + }, + "H6": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 59.38, + "y": 11.24, + "z": 25.49 + }, + "H7": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 68.38, + "y": 11.24, + "z": 25.49 + }, + "H8": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 77.38, + "y": 11.24, + "z": 25.49 + }, + "H9": { + "depth": 39.2, + "diameter": 3.27, + "shape": "circular", + "totalLiquidVolume": 20, + "x": 86.38, + "y": 11.24, + "z": 25.49 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadPipette", + "notes": [], + "params": { + "mount": "right", + "pipetteName": "p20_single_gen2" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 12.0, + "y": 8.75, + "z": 68.275 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 82.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Heater-Shaker Module GEN1", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -0.125, + "y": 1.125, + "z": 68.275 + }, + "model": "heaterShakerModuleV1", + "moduleType": "heaterShakerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + 0, + 0, + 0 + ], + [ + -1, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -49.325, + 0, + 0, + 1 + ], + [ + -1.125, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.125, + 1 + ] + ] + } + } + } + }, + "model": "heaterShakerModuleV1" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 11.7, + "y": 8.75, + "z": 80.09 + }, + "compatibleWith": [ + "temperatureModuleV1" + ], + "dimensions": { + "bareOverallHeight": 84.0, + "overLabwareHeight": 0.0 + }, + "displayName": "Temperature Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + } + }, + "labwareOffset": { + "x": -1.45, + "y": -0.15, + "z": 80.09 + }, + "model": "temperatureModuleV2", + "moduleType": "temperatureModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.15, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot2_standard": { + "3": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "6": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + }, + "9": { + "labwareOffset": [ + [ + -1, + -0.3, + 0, + 0 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + }, + "ot3_standard": { + "A1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "A3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "B3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "C3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D1": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + }, + "D3": { + "labwareOffset": [ + [ + -71.09, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0.15, + 1 + ], + [ + 0, + 0, + 1, + 1.45 + ] + ] + } + } + } + }, + "model": "temperatureModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadModule", + "notes": [], + "params": { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + }, + "result": { + "definition": { + "calibrationPoint": { + "x": 14.4, + "y": 64.93, + "z": 97.8 + }, + "compatibleWith": [], + "dimensions": { + "bareOverallHeight": 108.96, + "lidHeight": 61.7, + "overLabwareHeight": 0.0 + }, + "displayName": "Thermocycler Module GEN2", + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0.0, + "y": 0.0, + "z": 5.6 + }, + "pickUpOffset": { + "x": 0.0, + "y": 0.0, + "z": 4.6 + } + } + }, + "labwareOffset": { + "x": 0.0, + "y": 68.8, + "z": 108.96 + }, + "model": "thermocyclerModuleV2", + "moduleType": "thermocyclerModuleType", + "otSharedSchema": "module/schemas/2", + "quirks": [], + "slotTransforms": { + "ot3_standard": { + "B1": { + "cornerOffsetFromSlot": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ], + "labwareOffset": [ + [ + -98, + 0, + 0, + 1 + ], + [ + -20.005, + 0, + 0, + 1 + ], + [ + -0.84, + 0, + 0, + 1 + ], + [ + 0, + 0, + 0, + 1 + ] + ] + } + } + } + }, + "model": "thermocyclerModuleV2" + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_well_aluminum_block", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 18.16 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 Well Aluminum Block", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_well_aluminum_block", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 74.24, + "z": 3.38 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 74.24, + "z": 3.38 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 74.24, + "z": 3.38 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 74.24, + "z": 3.38 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 74.24, + "z": 3.38 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 74.24, + "z": 3.38 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 74.24, + "z": 3.38 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 74.24, + "z": 3.38 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 74.24, + "z": 3.38 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 74.24, + "z": 3.38 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 74.24, + "z": 3.38 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 74.24, + "z": 3.38 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 65.24, + "z": 3.38 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 65.24, + "z": 3.38 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 65.24, + "z": 3.38 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 65.24, + "z": 3.38 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 65.24, + "z": 3.38 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 65.24, + "z": 3.38 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 65.24, + "z": 3.38 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 65.24, + "z": 3.38 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 65.24, + "z": 3.38 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 65.24, + "z": 3.38 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 65.24, + "z": 3.38 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 65.24, + "z": 3.38 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 56.24, + "z": 3.38 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 56.24, + "z": 3.38 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 56.24, + "z": 3.38 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 56.24, + "z": 3.38 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 56.24, + "z": 3.38 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 56.24, + "z": 3.38 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 56.24, + "z": 3.38 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 56.24, + "z": 3.38 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 56.24, + "z": 3.38 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 56.24, + "z": 3.38 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 56.24, + "z": 3.38 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 56.24, + "z": 3.38 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 47.24, + "z": 3.38 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 47.24, + "z": 3.38 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 47.24, + "z": 3.38 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 47.24, + "z": 3.38 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 47.24, + "z": 3.38 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 47.24, + "z": 3.38 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 47.24, + "z": 3.38 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 47.24, + "z": 3.38 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 47.24, + "z": 3.38 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 47.24, + "z": 3.38 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 47.24, + "z": 3.38 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 47.24, + "z": 3.38 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 38.24, + "z": 3.38 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 38.24, + "z": 3.38 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 38.24, + "z": 3.38 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 38.24, + "z": 3.38 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 38.24, + "z": 3.38 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 38.24, + "z": 3.38 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 38.24, + "z": 3.38 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 38.24, + "z": 3.38 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 38.24, + "z": 3.38 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 38.24, + "z": 3.38 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 38.24, + "z": 3.38 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 38.24, + "z": 3.38 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 29.24, + "z": 3.38 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 29.24, + "z": 3.38 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 29.24, + "z": 3.38 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 29.24, + "z": 3.38 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 29.24, + "z": 3.38 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 29.24, + "z": 3.38 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 29.24, + "z": 3.38 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 29.24, + "z": 3.38 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 29.24, + "z": 3.38 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 29.24, + "z": 3.38 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 29.24, + "z": 3.38 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 29.24, + "z": 3.38 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 20.24, + "z": 3.38 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 20.24, + "z": 3.38 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 20.24, + "z": 3.38 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 20.24, + "z": 3.38 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 20.24, + "z": 3.38 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 20.24, + "z": 3.38 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 20.24, + "z": 3.38 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 20.24, + "z": 3.38 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 20.24, + "z": 3.38 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 20.24, + "z": 3.38 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 20.24, + "z": 3.38 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 20.24, + "z": 3.38 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 14.38, + "y": 11.24, + "z": 3.38 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 95.38, + "y": 11.24, + "z": 3.38 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 104.38, + "y": 11.24, + "z": 3.38 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 113.38, + "y": 11.24, + "z": 3.38 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 23.38, + "y": 11.24, + "z": 3.38 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 32.38, + "y": 11.24, + "z": 3.38 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 41.38, + "y": 11.24, + "z": 3.38 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 50.38, + "y": 11.24, + "z": 3.38 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 59.38, + "y": 11.24, + "z": 3.38 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 68.38, + "y": 11.24, + "z": 3.38 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 77.38, + "y": 11.24, + "z": 3.38 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 86.38, + "y": 11.24, + "z": 3.38 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "opentrons_96_pcr_adapter", + "location": {}, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [ + "adapter" + ], + "brand": { + "brand": "Opentrons", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 8.5, + "y": 5.5, + "z": 0 + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 13.85 + }, + "gripperOffsets": { + "default": { + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + }, + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + } + } + }, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "adapter", + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "opentrons_96_pcr_adapter", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 69, + "z": 1.85 + }, + "A10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 69, + "z": 1.85 + }, + "A11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 69, + "z": 1.85 + }, + "A12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 69, + "z": 1.85 + }, + "A2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 69, + "z": 1.85 + }, + "A3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 69, + "z": 1.85 + }, + "A4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 69, + "z": 1.85 + }, + "A5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 69, + "z": 1.85 + }, + "A6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 69, + "z": 1.85 + }, + "A7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 69, + "z": 1.85 + }, + "A8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 69, + "z": 1.85 + }, + "A9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 69, + "z": 1.85 + }, + "B1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 60, + "z": 1.85 + }, + "B10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 60, + "z": 1.85 + }, + "B11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 60, + "z": 1.85 + }, + "B12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 60, + "z": 1.85 + }, + "B2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 60, + "z": 1.85 + }, + "B3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 60, + "z": 1.85 + }, + "B4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 60, + "z": 1.85 + }, + "B5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 60, + "z": 1.85 + }, + "B6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 60, + "z": 1.85 + }, + "B7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 60, + "z": 1.85 + }, + "B8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 60, + "z": 1.85 + }, + "B9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 60, + "z": 1.85 + }, + "C1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 51, + "z": 1.85 + }, + "C10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 51, + "z": 1.85 + }, + "C11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 51, + "z": 1.85 + }, + "C12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 51, + "z": 1.85 + }, + "C2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 51, + "z": 1.85 + }, + "C3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 51, + "z": 1.85 + }, + "C4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 51, + "z": 1.85 + }, + "C5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 51, + "z": 1.85 + }, + "C6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 51, + "z": 1.85 + }, + "C7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 51, + "z": 1.85 + }, + "C8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 51, + "z": 1.85 + }, + "C9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 51, + "z": 1.85 + }, + "D1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 42, + "z": 1.85 + }, + "D10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 42, + "z": 1.85 + }, + "D11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 42, + "z": 1.85 + }, + "D12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 42, + "z": 1.85 + }, + "D2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 42, + "z": 1.85 + }, + "D3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 42, + "z": 1.85 + }, + "D4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 42, + "z": 1.85 + }, + "D5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 42, + "z": 1.85 + }, + "D6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 42, + "z": 1.85 + }, + "D7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 42, + "z": 1.85 + }, + "D8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 42, + "z": 1.85 + }, + "D9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 42, + "z": 1.85 + }, + "E1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 33, + "z": 1.85 + }, + "E10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 33, + "z": 1.85 + }, + "E11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 33, + "z": 1.85 + }, + "E12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 33, + "z": 1.85 + }, + "E2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 33, + "z": 1.85 + }, + "E3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 33, + "z": 1.85 + }, + "E4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 33, + "z": 1.85 + }, + "E5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 33, + "z": 1.85 + }, + "E6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 33, + "z": 1.85 + }, + "E7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 33, + "z": 1.85 + }, + "E8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 33, + "z": 1.85 + }, + "E9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 33, + "z": 1.85 + }, + "F1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 24, + "z": 1.85 + }, + "F10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 24, + "z": 1.85 + }, + "F11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 24, + "z": 1.85 + }, + "F12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 24, + "z": 1.85 + }, + "F2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 24, + "z": 1.85 + }, + "F3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 24, + "z": 1.85 + }, + "F4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 24, + "z": 1.85 + }, + "F5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 24, + "z": 1.85 + }, + "F6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 24, + "z": 1.85 + }, + "F7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 24, + "z": 1.85 + }, + "F8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 24, + "z": 1.85 + }, + "F9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 24, + "z": 1.85 + }, + "G1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 15, + "z": 1.85 + }, + "G10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 15, + "z": 1.85 + }, + "G11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 15, + "z": 1.85 + }, + "G12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 15, + "z": 1.85 + }, + "G2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 15, + "z": 1.85 + }, + "G3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 15, + "z": 1.85 + }, + "G4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 15, + "z": 1.85 + }, + "G5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 15, + "z": 1.85 + }, + "G6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 15, + "z": 1.85 + }, + "G7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 15, + "z": 1.85 + }, + "G8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 15, + "z": 1.85 + }, + "G9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 15, + "z": 1.85 + }, + "H1": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 6, + "y": 6, + "z": 1.85 + }, + "H10": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 87, + "y": 6, + "z": 1.85 + }, + "H11": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 96, + "y": 6, + "z": 1.85 + }, + "H12": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 105, + "y": 6, + "z": 1.85 + }, + "H2": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 15, + "y": 6, + "z": 1.85 + }, + "H3": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 24, + "y": 6, + "z": 1.85 + }, + "H4": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 33, + "y": 6, + "z": 1.85 + }, + "H5": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 42, + "y": 6, + "z": 1.85 + }, + "H6": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 51, + "y": 6, + "z": 1.85 + }, + "H7": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 60, + "y": 6, + "z": 1.85 + }, + "H8": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 69, + "y": 6, + "z": 1.85 + }, + "H9": { + "depth": 12, + "diameter": 5.64, + "shape": "circular", + "totalLiquidVolume": 0, + "x": 78, + "y": 6, + "z": 1.85 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {}, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + }, + "namespace": "custom_beta", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "cpx", + "brandId": [] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127, + "yDimension": 85, + "zDimension": 40 + }, + "gripperOffsets": {}, + "groups": [ + { + "brand": { + "brand": "cpx", + "brandId": [] + }, + "metadata": { + "displayCategory": "tubeRack", + "wellBottomShape": "u" + }, + "wells": [ + "A1", + "A2", + "B1", + "B2" + ] + } + ], + "metadata": { + "displayCategory": "tubeRack", + "displayName": "cpx 4 Tube Rack with cpx 0.1 mL", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "custom_beta", + "ordering": [ + [ + "A1", + "B1" + ], + [ + "A2", + "B2" + ] + ], + "parameters": { + "format": "irregular", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "cpx_4_tuberack_100ul", + "quirks": [] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 65, + "z": 17 + }, + "A2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 65, + "z": 17 + }, + "B1": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 20, + "y": 35, + "z": 17 + }, + "B2": { + "depth": 23, + "diameter": 20, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50, + "y": 35, + "z": 17 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + }, + "namespace": "opentrons", + "version": 2 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "402501" + ], + "links": [ + "https://www.nest-biotech.com/pcr-plates/58773587.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 15.7 + }, + "gripForce": 15.0, + "gripHeightFromLabwareBottom": 10.65, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "B1", + "B10", + "B11", + "B12", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "C1", + "C10", + "C11", + "C12", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "D1", + "D10", + "D11", + "D12", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "E1", + "E10", + "E11", + "E12", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "F1", + "F10", + "F11", + "F12", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "G1", + "G10", + "G11", + "G12", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "H1", + "H10", + "H11", + "H12", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9" + ] + } + ], + "metadata": { + "displayCategory": "wellPlate", + "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", + "displayVolumeUnits": "µL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1" + ], + [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10" + ], + [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11" + ], + [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2" + ], + [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3" + ], + [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4" + ], + [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5" + ], + [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6" + ], + [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7" + ], + [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8" + ], + [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9" + ] + ], + "parameters": { + "format": "96Standard", + "isMagneticModuleCompatible": true, + "isTiprack": false, + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "magneticModuleEngageHeight": 20 + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.2 + }, + "opentrons_96_well_aluminum_block": { + "x": 0, + "y": 0, + "z": 12.66 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { + "x": 0, + "y": 0, + "z": 10.8 + } + }, + "version": 2, + "wells": { + "A1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 74.24, + "z": 0.92 + }, + "A10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 74.24, + "z": 0.92 + }, + "A11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 74.24, + "z": 0.92 + }, + "A12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 74.24, + "z": 0.92 + }, + "A2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 74.24, + "z": 0.92 + }, + "A3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 74.24, + "z": 0.92 + }, + "A4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 74.24, + "z": 0.92 + }, + "A5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 74.24, + "z": 0.92 + }, + "A6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 74.24, + "z": 0.92 + }, + "A7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 74.24, + "z": 0.92 + }, + "A8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 74.24, + "z": 0.92 + }, + "A9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 74.24, + "z": 0.92 + }, + "B1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 65.24, + "z": 0.92 + }, + "B10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 65.24, + "z": 0.92 + }, + "B11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 65.24, + "z": 0.92 + }, + "B12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 65.24, + "z": 0.92 + }, + "B2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 65.24, + "z": 0.92 + }, + "B3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 65.24, + "z": 0.92 + }, + "B4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 65.24, + "z": 0.92 + }, + "B5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 65.24, + "z": 0.92 + }, + "B6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 65.24, + "z": 0.92 + }, + "B7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 65.24, + "z": 0.92 + }, + "B8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 65.24, + "z": 0.92 + }, + "B9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 65.24, + "z": 0.92 + }, + "C1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 56.24, + "z": 0.92 + }, + "C10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 56.24, + "z": 0.92 + }, + "C11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 56.24, + "z": 0.92 + }, + "C12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 56.24, + "z": 0.92 + }, + "C2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 56.24, + "z": 0.92 + }, + "C3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 56.24, + "z": 0.92 + }, + "C4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 56.24, + "z": 0.92 + }, + "C5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 56.24, + "z": 0.92 + }, + "C6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 56.24, + "z": 0.92 + }, + "C7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 56.24, + "z": 0.92 + }, + "C8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 56.24, + "z": 0.92 + }, + "C9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 56.24, + "z": 0.92 + }, + "D1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 47.24, + "z": 0.92 + }, + "D10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 47.24, + "z": 0.92 + }, + "D11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 47.24, + "z": 0.92 + }, + "D12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 47.24, + "z": 0.92 + }, + "D2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 47.24, + "z": 0.92 + }, + "D3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 47.24, + "z": 0.92 + }, + "D4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 47.24, + "z": 0.92 + }, + "D5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 47.24, + "z": 0.92 + }, + "D6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 47.24, + "z": 0.92 + }, + "D7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 47.24, + "z": 0.92 + }, + "D8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 47.24, + "z": 0.92 + }, + "D9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 47.24, + "z": 0.92 + }, + "E1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 38.24, + "z": 0.92 + }, + "E10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 38.24, + "z": 0.92 + }, + "E11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 38.24, + "z": 0.92 + }, + "E12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 38.24, + "z": 0.92 + }, + "E2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 38.24, + "z": 0.92 + }, + "E3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 38.24, + "z": 0.92 + }, + "E4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 38.24, + "z": 0.92 + }, + "E5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 38.24, + "z": 0.92 + }, + "E6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 38.24, + "z": 0.92 + }, + "E7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 38.24, + "z": 0.92 + }, + "E8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 38.24, + "z": 0.92 + }, + "E9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 38.24, + "z": 0.92 + }, + "F1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 29.24, + "z": 0.92 + }, + "F10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 29.24, + "z": 0.92 + }, + "F11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 29.24, + "z": 0.92 + }, + "F12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 29.24, + "z": 0.92 + }, + "F2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 29.24, + "z": 0.92 + }, + "F3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 29.24, + "z": 0.92 + }, + "F4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 29.24, + "z": 0.92 + }, + "F5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 29.24, + "z": 0.92 + }, + "F6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 29.24, + "z": 0.92 + }, + "F7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 29.24, + "z": 0.92 + }, + "F8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 29.24, + "z": 0.92 + }, + "F9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 29.24, + "z": 0.92 + }, + "G1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 20.24, + "z": 0.92 + }, + "G10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 20.24, + "z": 0.92 + }, + "G11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 20.24, + "z": 0.92 + }, + "G12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 20.24, + "z": 0.92 + }, + "G2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 20.24, + "z": 0.92 + }, + "G3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 20.24, + "z": 0.92 + }, + "G4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 20.24, + "z": 0.92 + }, + "G5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 20.24, + "z": 0.92 + }, + "G6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 20.24, + "z": 0.92 + }, + "G7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 20.24, + "z": 0.92 + }, + "G8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 20.24, + "z": 0.92 + }, + "G9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 20.24, + "z": 0.92 + }, + "H1": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 14.38, + "y": 11.24, + "z": 0.92 + }, + "H10": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 95.38, + "y": 11.24, + "z": 0.92 + }, + "H11": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 104.38, + "y": 11.24, + "z": 0.92 + }, + "H12": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 113.38, + "y": 11.24, + "z": 0.92 + }, + "H2": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 23.38, + "y": 11.24, + "z": 0.92 + }, + "H3": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 32.38, + "y": 11.24, + "z": 0.92 + }, + "H4": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 41.38, + "y": 11.24, + "z": 0.92 + }, + "H5": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 50.38, + "y": 11.24, + "z": 0.92 + }, + "H6": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 59.38, + "y": 11.24, + "z": 0.92 + }, + "H7": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 68.38, + "y": 11.24, + "z": 0.92 + }, + "H8": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 77.38, + "y": 11.24, + "z": 0.92 + }, + "H9": { + "depth": 14.78, + "diameter": 5.34, + "shape": "circular", + "totalLiquidVolume": 100, + "x": 86.38, + "y": 11.24, + "z": 0.92 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLabware", + "notes": [], + "params": { + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + }, + "namespace": "opentrons", + "version": 1 + }, + "result": { + "definition": { + "allowedRoles": [], + "brand": { + "brand": "NEST", + "brandId": [ + "360102" + ], + "links": [ + "https://www.nest-biotech.com/reagent-reserviors/59178414.html" + ] + }, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 31.4 + }, + "gripperOffsets": {}, + "groups": [ + { + "metadata": { + "wellBottomShape": "v" + }, + "wells": [ + "A1", + "A10", + "A11", + "A12", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9" + ] + } + ], + "metadata": { + "displayCategory": "reservoir", + "displayName": "NEST 12 Well Reservoir 15 mL", + "displayVolumeUnits": "mL", + "tags": [] + }, + "namespace": "opentrons", + "ordering": [ + [ + "A1" + ], + [ + "A10" + ], + [ + "A11" + ], + [ + "A12" + ], + [ + "A2" + ], + [ + "A3" + ], + [ + "A4" + ], + [ + "A5" + ], + [ + "A6" + ], + [ + "A7" + ], + [ + "A8" + ], + [ + "A9" + ] + ], + "parameters": { + "format": "trough", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "nest_12_reservoir_15ml", + "quirks": [ + "centerMultichannelOnWells", + "touchTipDisabled" + ] + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": {}, + "stackingOffsetWithModule": {}, + "version": 1, + "wells": { + "A1": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 14.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A10": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 95.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A11": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 104.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A12": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 113.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A2": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 23.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A3": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 32.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A4": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 41.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A5": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 50.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A6": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 59.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A7": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 68.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A8": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 77.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + }, + "A9": { + "depth": 26.85, + "shape": "rectangular", + "totalLiquidVolume": 15000, + "x": 86.38, + "xDimension": 8.2, + "y": 42.78, + "yDimension": 71.2, + "z": 4.55 + } + } + } + }, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A1": 4000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A2": 2000.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A5": 555.55555 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 900.0 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "loadLiquid", + "notes": [], + "params": { + "volumeByWell": { + "A8": 1001.11 + } + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/closeLabwareLatch", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.38, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": "offDeck", + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "3" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of reservoir A1 in slot 3?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 152.5, + "y": 65.0, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "6" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 285.0, + "y": 155.5, + "z": 40.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of custom labware A1 in slot 6?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveLabware", + "notes": [], + "params": { + "newLocation": { + "slotName": "2" + }, + "strategy": "manualMoveWithPause" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of well A1 in slot 2?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "prepareToAspirate", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Testing prepare_to_aspirate - watch pipette until next pause.\n The pipette should only move up out of the well after it has aspirated." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Did the pipette move up out of the well, only once, after aspirating?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -24.85 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 279.38, + "y": 42.78, + "z": 6.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash? Pipette will home after this pause." + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "home", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette over the trash?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C7" + }, + "result": { + "position": { + "x": 200.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D6" + }, + "result": { + "position": { + "x": 191.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D7" + }, + "result": { + "position": { + "x": 200.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "D8" + }, + "result": { + "position": { + "x": 209.88, + "y": 47.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 19.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 19.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 18.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "E5" + }, + "result": { + "position": { + "x": 182.88, + "y": 38.24, + "z": 1.92 + }, + "volume": 18.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": false, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 347.84000000000003, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowOutInPlace", + "notes": [], + "params": { + "flowRate": 7.56 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "pushOut": 0.0, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "B11" + }, + "result": { + "position": { + "x": 236.88, + "y": 65.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "C11" + }, + "result": { + "position": { + "x": 236.88, + "y": 56.24, + "z": 1.92 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 20.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A5" + }, + "result": { + "position": { + "x": 315.38, + "y": 42.78, + "z": 31.400000000000002 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "B1" + }, + "result": { + "position": { + "x": 14.38, + "y": 155.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + } + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 5.0000000000000036 + }, + "origin": "top" + }, + "wellName": "A11" + }, + "result": { + "position": { + "x": 236.88, + "y": 74.24, + "z": 20.700000000000003 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 3.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 5.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.78 + }, + "origin": "top" + }, + "wellName": "H11" + }, + "result": { + "position": { + "x": 236.88, + "y": 11.24, + "z": 1.92 + }, + "volume": 5.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E12" + }, + "result": { + "position": { + "x": 245.88, + "y": 38.24, + "z": 15.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "moveToWell", + "notes": [], + "params": { + "forceDirect": false, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "blowout", + "notes": [], + "params": { + "flowRate": 7.56, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -14.78 + }, + "origin": "top" + }, + "wellName": "E11" + }, + "result": { + "position": { + "x": 236.88, + "y": 38.24, + "z": 0.92 + } + }, + "status": "succeeded" + }, + { + "commandType": "touchTip", + "notes": [], + "params": { + "radius": 1.0, + "speed": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -1.0 + }, + "origin": "top" + }, + "wellName": "H1" + }, + "result": { + "position": { + "x": 146.88, + "y": 11.24, + "z": 14.7 + } + }, + "status": "succeeded" + }, + { + "commandType": "waitForResume", + "notes": [], + "params": { + "message": "Is the pipette tip in the middle of the well?" + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "dropTip", + "notes": [], + "params": { + "alternateDropLocation": false, + "wellLocation": { + "offset": { + "x": 0, + "y": 0, + "z": 0 + }, + "origin": "default" + }, + "wellName": "C1" + }, + "result": { + "position": { + "x": 14.38, + "y": 146.74, + "z": 45.09 + } + }, + "status": "succeeded" + }, + { + "commandType": "temperatureModule/waitForTemperature", + "notes": [], + "params": { + "celsius": 25.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 466.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setTargetTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/waitForTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/closeLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetLidTemperature", + "notes": [], + "params": { + "celsius": 38.0 + }, + "result": { + "targetLidTemperature": 38.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForLidTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/setTargetBlockTemperature", + "notes": [], + "params": { + "celsius": 28.0, + "holdTimeSeconds": 5.0 + }, + "result": { + "targetBlockTemperature": 28.0 + }, + "status": "succeeded" + }, + { + "commandType": "thermocycler/waitForBlockTemperature", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateBlock", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/deactivateLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "thermocycler/openLid", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "D1" + }, + "result": { + "position": { + "x": 14.38, + "y": 137.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 7.56, + "volume": 15.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 280.53, + "y": 255.08999999999997, + "z": 87.51 + }, + "volume": 15.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 146.88, + "y": 164.74, + "z": 64.69 + }, + "tipDiameter": 5.23, + "tipLength": 51.099999999999994, + "tipVolume": 300.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 50.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A1" + }, + "result": { + "position": { + "x": 14.255, + "y": 75.365, + "z": 73.84500000000001 + }, + "volume": 50.0 + }, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "notes": [], + "params": { + "rpm": 350.0 + }, + "result": { + "pipetteRetracted": true + }, + "status": "succeeded" + }, + { + "commandType": "waitForDuration", + "notes": [], + "params": { + "seconds": 5.0 + }, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "heaterShaker/deactivateShaker", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "pickUpTip", + "notes": [], + "params": { + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "origin": "top" + }, + "wellName": "E1" + }, + "result": { + "position": { + "x": 14.38, + "y": 128.74, + "z": 64.69 + }, + "tipDiameter": 3.27, + "tipLength": 30.950000000000003, + "tipVolume": 20.0 + }, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 15.12, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 11.34, + "volume": 10.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -22.0 + }, + "origin": "top" + }, + "wellName": "B2" + }, + "result": { + "position": { + "x": 315.0, + "y": 125.5, + "z": 18.0 + }, + "volume": 10.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 363.89500000000004, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" + }, + { + "commandType": "aspirate", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 75.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -25.85 + }, + "origin": "top" + }, + "wellName": "A2" + }, + "result": { + "position": { + "x": 288.38, + "y": 42.78, + "z": 5.55 + }, + "volume": 75.0 + }, + "status": "succeeded" + }, + { + "commandType": "dispense", + "notes": [], + "params": { + "flowRate": 94.0, + "volume": 60.0, + "wellLocation": { + "offset": { + "x": 0.0, + "y": 0.0, + "z": -13.780000000000001 + }, + "origin": "top" + }, + "wellName": "A6" + }, + "result": { + "position": { + "x": 59.38, + "y": 324.04, + "z": 100.08 + }, + "volume": 60.0 + }, + "status": "succeeded" + }, + { + "commandType": "moveToAddressableAreaForDropTip", + "notes": [], + "params": { + "addressableAreaName": "fixedTrash", + "alternateDropLocation": true, + "forceDirect": false, + "ignoreTipConfiguration": true, + "offset": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + } + }, + "result": { + "position": { + "x": 331.785, + "y": 351.5, + "z": 82.0 + } + }, + "status": "succeeded" + }, + { + "commandType": "dropTipInPlace", + "notes": [], + "params": {}, + "result": {}, + "status": "succeeded" } ], "config": { "apiVersion": [ 2, - 16 + 17 ], "protocolType": "python" }, @@ -42,16 +15573,122 @@ "role": "labware" } ], - "labware": [], - "liquids": [], + "labware": [ + { + "definitionUri": "opentrons/opentrons_96_tiprack_300ul/1", + "displayName": "300ul tips", + "loadName": "opentrons_96_tiprack_300ul", + "location": { + "slotName": "5" + } + }, + { + "definitionUri": "opentrons/opentrons_96_tiprack_20ul/1", + "displayName": "20ul tips", + "loadName": "opentrons_96_tiprack_20ul", + "location": { + "slotName": "4" + } + }, + { + "definitionUri": "opentrons/opentrons_96_well_aluminum_block/1", + "loadName": "opentrons_96_well_aluminum_block", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "Temperature-Controlled plate", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": {} + }, + { + "definitionUri": "custom_beta/cpx_4_tuberack_100ul/1", + "displayName": "4 custom tubes", + "loadName": "cpx_4_tuberack_100ul", + "location": { + "slotName": "6" + } + }, + { + "definitionUri": "opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", + "displayName": "logo destination", + "loadName": "nest_96_wellplate_100ul_pcr_full_skirt", + "location": { + "slotName": "2" + } + }, + { + "definitionUri": "opentrons/nest_12_reservoir_15ml/1", + "displayName": "dye container", + "loadName": "nest_12_reservoir_15ml", + "location": { + "slotName": "3" + } + } + ], + "liquids": [ + { + "description": "H₂O", + "displayColor": "#42AB2D", + "displayName": "water" + }, + { + "description": "C₃H₆O", + "displayColor": "#38588a", + "displayName": "acetone" + } + ], "metadata": { "author": "Opentrons Engineering ", - "description": "Placeholder - 2.17 Smoke Test is the same a 2.16 Smoke Test.", - "protocolName": "🛠️ 2.17 Smoke Test", + "description": "Description of the protocol that is longish \n has \n returns and \n emoji 😊 ⬆️ ", + "protocolName": "🛠️ 2.17 Smoke Test V3 🪄", "source": "Software Testing Team" }, - "modules": [], - "pipettes": [], + "modules": [ + { + "location": { + "slotName": "1" + }, + "model": "heaterShakerModuleV1" + }, + { + "location": { + "slotName": "9" + }, + "model": "temperatureModuleV2" + }, + { + "location": { + "slotName": "7" + }, + "model": "thermocyclerModuleV2" + } + ], + "pipettes": [ + { + "mount": "left", + "pipetteName": "p300_multi_gen2" + }, + { + "mount": "right", + "pipetteName": "p20_single_gen2" + } + ], "robotType": "OT-2 Standard", "runTimeParameters": [] } diff --git a/app/README.md b/app/README.md index f73f215a48a7..93bf6182ed92 100644 --- a/app/README.md +++ b/app/README.md @@ -27,7 +27,7 @@ make -C app dev **Note:** If you would like to interact with a virtual robot server being served at `localhost`, you will need to manually add `localhost` to the discovery candidates list. This can be done through the app's GUI settings for "Connect to a robot via IP address / Add Manual IP Address" -At this point, the Electron app will be running with [HMR][] and various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): +At this point, the Electron app will be running with various Chrome devtools enabled. The app and dev server look for the following environment variables (defaults set in Makefile): | Variable | Default | Description | | -------------------- | ------------ | --------------------------------------------------- | @@ -46,7 +46,7 @@ The UI stack is built using: - [Redux][] - [CSS modules][css-modules] - [Babel][] -- [Webpack][] +- [Vite][] Some important directories: @@ -54,7 +54,6 @@ Some important directories: - API clients (see [`api/opentrons/server`][api-server-source]) - `api-client` - HTTP Robot API client - `react-api-client` - react utilities for Robot API client -- `app/webpack` - Webpack configuration helpers ## Copy management @@ -131,10 +130,9 @@ ANALYZER=1 make -C app [api-server-source]: ../api/opentrons/server [electron]: https://www.electronjs.org/ [electron-renderer]: https://electronjs.org/docs/tutorial/quick-start#renderer-process -[hmr]: https://webpack.js.org/concepts/hot-module-replacement/ [react]: https://react.dev/ [redux]: http://redux.js.org/ [css-modules]: https://github.com/css-modules/css-modules [babel]: https://babeljs.io/ -[webpack]: https://webpack.js.org/ +[vite]: https://vitejs.dev/ [bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer diff --git a/app/package.json b/app/package.json index f72519e3f4af..30836e11b8ed 100644 --- a/app/package.json +++ b/app/package.json @@ -49,16 +49,15 @@ "react-error-boundary": "^4.0.10", "react-i18next": "13.5.0", "react-intersection-observer": "^8.33.1", + "react-markdown": "9.0.1", "react-redux": "8.1.2", "react-router-dom": "5.3.4", "react-select": "5.4.0", - "react-simple-keyboard": "^3.4.187", + "react-simple-keyboard": "^3.7.0", "react-viewport-list": "6.3.0", "redux": "4.0.5", "redux-observable": "1.1.0", "redux-thunk": "2.3.0", - "remark": "9.0.0", - "remark-react": "4.0.3", "reselect": "4.0.0", "rxjs": "^6.5.1", "semver": "5.5.0", diff --git a/app/src/App/DesktopApp.tsx b/app/src/App/DesktopApp.tsx index f42ef7e0e809..ffa50727da16 100644 --- a/app/src/App/DesktopApp.tsx +++ b/app/src/App/DesktopApp.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom' import { ErrorBoundary } from 'react-error-boundary' +import { I18nextProvider } from 'react-i18next' import { Box, @@ -11,6 +12,7 @@ import { import { ApiHostProvider } from '@opentrons/react-api-client' import NiceModal from '@ebay/nice-modal-react' +import { i18n } from '../i18n' import { Alerts } from '../organisms/Alerts' import { Breadcrumbs } from '../organisms/Breadcrumbs' import { ToasterOven } from '../organisms/ToasterOven' @@ -101,45 +103,47 @@ export const DesktopApp = (): JSX.Element => { return ( - - - - - - - - {desktopRoutes.map( - ({ Component, exact, path }: RouteProps) => { - return ( - - - - - - - - ) - } - )} - - - - - - - - + + + + + + + + + {desktopRoutes.map( + ({ Component, exact, path }: RouteProps) => { + return ( + + + + + + + + ) + } + )} + + + + + + + + + ) } diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index 8397927392f5..f9e79ea65e9d 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -28,6 +28,7 @@ import { NAV_BAR_WIDTH } from './constants' import type { RouteProps } from './types' const SALESFORCE_HELP_LINK = 'https://support.opentrons.com/s/' +const PROJECT: string = _OPENTRONS_PROJECT_ const NavbarLink = styled(NavLink)` color: ${COLORS.white}; @@ -128,7 +129,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { alignSelf={ALIGN_STRETCH} > {navRoutes.map(({ name, navLinkTo }: RouteProps) => ( diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index c9923e1aea37..1459ff5071f8 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -16,6 +16,7 @@ import { ApiHostProvider } from '@opentrons/react-api-client' import NiceModal from '@ebay/nice-modal-react' import { SleepScreen } from '../atoms/SleepScreen' +import { OnDeviceLocalizationProvider } from '../LocalizationProvider' import { ToasterOven } from '../organisms/ToasterOven' import { MaintenanceRunTakeover } from '../organisms/TakeoverModal' import { FirmwareUpdateTakeover } from '../organisms/FirmwareUpdateModal/FirmwareUpdateTakeover' @@ -31,6 +32,7 @@ import { RobotDashboard } from '../pages/RobotDashboard' import { RobotSettingsDashboard } from '../pages/RobotSettingsDashboard' import { ProtocolDashboard } from '../pages/ProtocolDashboard' import { ProtocolDetails } from '../pages/ProtocolDetails' +import { QuickTransferFlow } from '../organisms/QuickTransferFlow' import { RunningProtocol } from '../pages/RunningProtocol' import { RunSummary } from '../pages/RunSummary' import { UpdateRobot } from '../pages/UpdateRobot/UpdateRobot' @@ -66,13 +68,13 @@ export const ON_DEVICE_DISPLAY_PATHS = [ '/emergency-stop', '/instruments', '/instruments/:mount', - '/loading', '/network-setup', '/network-setup/ethernet', '/network-setup/usb', '/network-setup/wifi', '/protocols', '/protocols/:protocolId', + '/quick-transfer', '/robot-settings', '/robot-settings/rename-robot', '/robot-settings/update-robot', @@ -97,8 +99,6 @@ function getPathComponent( return case '/instruments/:mount': return - case '/loading': - return case '/network-setup': return case '/network-setup/ethernet': @@ -111,6 +111,8 @@ function getPathComponent( return case '/protocols/:protocolId': return + case `/quick-transfer`: + return case '/robot-settings': return case '/robot-settings/rename-robot': @@ -151,12 +153,75 @@ export const OnDeviceDisplayApp = (): JSX.Element => { } const dispatch = useDispatch() const isIdle = useIdle(sleepTime, options) + + React.useEffect(() => { + if (isIdle) { + dispatch(updateBrightness(TURN_OFF_BACKLIGHT)) + } else { + dispatch( + updateConfigValue( + 'onDeviceDisplaySettings.brightness', + userSetBrightness + ) + ) + } + }, [dispatch, isIdle, userSetBrightness]) + + // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals + return ( + + + + + + {isIdle ? ( + + ) : ( + <> + + + + + + + + + + + + )} + + + + + + + ) +} + +const getTargetPath = (unfinishedUnboxingFlowRoute: string | null): string => { + if (unfinishedUnboxingFlowRoute != null) { + return unfinishedUnboxingFlowRoute + } + + return '/dashboard' +} + +// split to a separate function because scrollRef rerenders on every route change +// this avoids rerendering parent providers as well +export function OnDeviceDisplayAppRoutes(): JSX.Element { const [currentNode, setCurrentNode] = React.useState(null) const scrollRef = React.useCallback((node: HTMLElement | null) => { setCurrentNode(node) }, []) const isScrolling = useScrolling(currentNode) + const { unfinishedUnboxingFlowRoute } = useSelector( + getOnDeviceDisplaySettings + ) + + const targetPath = getTargetPath(unfinishedUnboxingFlowRoute) + const TOUCH_SCREEN_STYLE = css` position: ${POSITION_RELATIVE}; width: 100%; @@ -176,54 +241,18 @@ export const OnDeviceDisplayApp = (): JSX.Element => { } ` - React.useEffect(() => { - if (isIdle) { - dispatch(updateBrightness(TURN_OFF_BACKLIGHT)) - } else { - dispatch( - updateConfigValue( - 'onDeviceDisplaySettings.brightness', - userSetBrightness - ) - ) - } - }, [dispatch, isIdle, userSetBrightness]) - - // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( - - - - {isIdle ? ( - - ) : ( - <> - - - - - - - - {ON_DEVICE_DISPLAY_PATHS.map(path => ( - - - - {getPathComponent(path)} - - - ))} - - - - - - - )} - - - - + + {ON_DEVICE_DISPLAY_PATHS.map(path => ( + + + + {getPathComponent(path)} + + + ))} + {targetPath != null && } + ) } diff --git a/app/src/App/OnDeviceDisplayAppFallback.tsx b/app/src/App/OnDeviceDisplayAppFallback.tsx index 6a345c1735e5..0e48a31e5659 100644 --- a/app/src/App/OnDeviceDisplayAppFallback.tsx +++ b/app/src/App/OnDeviceDisplayAppFallback.tsx @@ -27,7 +27,7 @@ import type { ModalHeaderBaseProps } from '../molecules/Modal/types' export function OnDeviceDisplayAppFallback({ error, }: FallbackProps): JSX.Element { - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const trackEvent = useTrackEvent() const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) @@ -59,7 +59,9 @@ export function OnDeviceDisplayAppFallback({ alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER} > - {t('error_boundary_description')} + + {t('branded:error_boundary_description')} + { + const actual = await vi.importActual('@opentrons/react-api-client') + return { + ...actual, + useRobotSettingsQuery: () => + (({ + data: { settings: [] }, + } as unknown) as UseQueryResult), + } +}) +vi.mock('../../LocalizationProvider') vi.mock('../../pages/Welcome') vi.mock('../../pages/NetworkSetupMenu') vi.mock('../../pages/ConnectViaEthernet') @@ -45,7 +60,6 @@ vi.mock('../../pages/InstrumentsDashboard') vi.mock('../../pages/RunningProtocol') vi.mock('../../pages/RunSummary') vi.mock('../../pages/NameRobot') -vi.mock('../../pages/InitialLoadingScreen') vi.mock('../../pages/EmergencyStop') vi.mock('../../pages/DeckConfiguration') vi.mock('../../redux/config') @@ -73,7 +87,7 @@ const render = (path = '/') => { describe('OnDeviceDisplayApp', () => { beforeEach(() => { vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockSettings as any) - vi.mocked(getIsShellReady).mockReturnValue(false) + vi.mocked(getIsShellReady).mockReturnValue(true) vi.mocked(useCurrentRunRoute).mockReturnValue(null) vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(useNotifyCurrentMaintenanceRun).mockReturnValue({ @@ -83,6 +97,12 @@ describe('OnDeviceDisplayApp', () => { }, }, } as any) + // TODO(bh, 2024-03-27): implement testing of branded and anonymous i18n, but for now pass through + vi.mocked( + OnDeviceLocalizationProvider + ).mockImplementation((props: OnDeviceLocalizationProviderProps) => ( + <>{props.children} + )) }) afterEach(() => { vi.resetAllMocks() @@ -140,9 +160,16 @@ describe('OnDeviceDisplayApp', () => { render('/runs/my-run-id/summary') expect(vi.mocked(RunSummary)).toHaveBeenCalled() }) - it('renders the loading screen on mount', () => { - render('/loading') - expect(vi.mocked(InitialLoadingScreen)).toHaveBeenCalled() + it('renders the localization provider and not the loading screen when app-shell is ready', () => { + render('/') + expect(vi.mocked(OnDeviceLocalizationProvider)).toHaveBeenCalled() + expect(screen.queryByLabelText('loading indicator')).toBeNull() + }) + it('renders the loading screen when app-shell is not ready', () => { + vi.mocked(getIsShellReady).mockReturnValue(false) + render('/') + screen.getByLabelText('loading indicator') + expect(vi.mocked(OnDeviceLocalizationProvider)).not.toHaveBeenCalled() }) it('renders EmergencyStop component from /emergency-stop', () => { render('/emergency-stop') diff --git a/app/src/LocalizationProvider.tsx b/app/src/LocalizationProvider.tsx new file mode 100644 index 000000000000..4b9e10a1f8d2 --- /dev/null +++ b/app/src/LocalizationProvider.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { I18nextProvider } from 'react-i18next' +import reduce from 'lodash/reduce' + +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + +import { resources } from './assets/localization' +import { i18n, i18nCb, i18nConfig } from './i18n' + +import type { RobotSettingsField } from '@opentrons/api-client' + +export interface OnDeviceLocalizationProviderProps { + children?: React.ReactNode +} + +const BRANDED_RESOURCE = 'branded' +const ANONYMOUS_RESOURCE = 'anonymous' + +// TODO(bh, 2024-03-26): anonymization limited to ODD for now, may change in future OEM phases +export function OnDeviceLocalizationProvider( + props: OnDeviceLocalizationProviderProps +): JSX.Element | null { + const { settings } = useRobotSettingsQuery().data ?? {} + const oemModeSetting = (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + ) + const isOEMMode = oemModeSetting?.value ?? false + + // iterate through language resources, nested files, substitute anonymous file for branded file for OEM mode + const anonResources = reduce( + resources, + (acc, resource, language) => { + const anonFiles = reduce( + resource, + (acc, file, fileName) => { + if (fileName === BRANDED_RESOURCE && isOEMMode) { + return acc + } else if (fileName === ANONYMOUS_RESOURCE) { + return isOEMMode ? { ...acc, [BRANDED_RESOURCE]: file } : acc + } else { + return { ...acc, [fileName]: file } + } + }, + {} + ) + return { ...acc, [language]: anonFiles } + }, + {} + ) + + const anonI18n = i18n.createInstance( + { + ...i18nConfig, + resources: anonResources, + }, + i18nCb + ) + + return {props.children} +} diff --git a/app/src/assets/images/on-device-display/multiple_modules_modal.png b/app/src/assets/images/on-device-display/multiple_modules_modal.png deleted file mode 100644 index 721c7cb11f7f..000000000000 Binary files a/app/src/assets/images/on-device-display/multiple_modules_modal.png and /dev/null differ diff --git a/app/src/assets/localization/en/anonymous.json b/app/src/assets/localization/en/anonymous.json new file mode 100644 index 000000000000..5dcfd9bf2371 --- /dev/null +++ b/app/src/assets/localization/en/anonymous.json @@ -0,0 +1,73 @@ +{ + "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the desktop app. Go to Robot", + "about_flex_gripper": "About Gripper", + "alternative_security_types_description": "The robot supports connecting to various enterprise access points. Connect via USB and finish setup in the desktop app.", + "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", + "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", + "choose_what_data_to_share": "Choose what robot data to share.", + "computer_in_app_is_controlling_robot": "A network-connected computer is currently controlling this robot.", + "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error on that computer.", + "connect_and_screw_in_gripper": "Connect and secure gripper", + "connect_via_usb_description_3": "3. Launch the robot app on the connected computer to continue.", + "connection_description_usb": "Connect directly to a computer.", + "connection_lost_description": "The app is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot and then try to reconnect.", + "contact_information": "Contact support for assistance.", + "contact_support_for_connection_help": "If none of these work, contact support for help (via the question mark link in this app, or by emailing {{support_email}}.)", + "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, contact support.", + "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the desktop app.", + "error_boundary_description": "You need to restart the touchscreen. Contact support for assistance.", + "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have the robot move the gantry to its home position.", + "find_your_robot": "Find your robot in the Devices section of the app to install software updates.", + "firmware_update_download_logs": "Contact support for assistance.", + "general_error_message": "If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact support.", + "gripper_still_attached": "Gripper still attached", + "gripper_successfully_attached_and_calibrated": "Gripper successfully attached and calibrated", + "gripper_successfully_calibrated": "Gripper successfully calibrated", + "gripper_successfully_detached": "Gripper successfully detached", + "gripper": "Gripper", + "ip_description_second": "Work with your network administrator to assign a static IP address to the robot.", + "learn_uninstalling": "Learn more about uninstalling the app", + "loosen_screws_and_detach": "Loosen screws and detach gripper", + "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact support.{{error}}", + "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your pipette.", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact support.", + "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your robot.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", + "opentrons_app_successfully_updated": "The app was successfully updated.", + "opentrons_app_update": "app update", + "opentrons_app_update_available": "App Update Available", + "opentrons_app_update_available_variation": "An app update is available.", + "opentrons_app_will_use_interpreter": "If specified, the app will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_cares_about_privacy": "We care about your privacy. We anonymize all data and only use it to improve our products.", + "opentrons_def": "Verified Definition", + "opentrons_labware_def": "Verified labware definition", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", + "opentrons_tip_rack_name": "opentrons", + "previous_releases": "View previous releases", + "receive_alert": "Receive an alert when a software update is available.", + "restore_description": "Reverting to previous software versions is not recommended, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", + "robot_server_version_ot3_description": "The robot software includes the robot server and the touchscreen display interface.", + "robot_software_update_required": "A robot software update is required to run protocols with this version of the app.", + "run_failed_modal_description_desktop": "Contact support for assistance.", + "secure_labware_explanation_magnetic_module": "Ensure that your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Secure your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "send_a_protocol_to_store": "Send a protocol to the robot to get started.", + "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box.", + "share_app_analytics": "Share App Analytics", + "share_app_analytics_description": "Help improve this product by automatically sending anonymous diagnostics and usage data.", + "share_display_usage_description": "Data on how you interact with the robot's touchscreen.", + "share_logs_with_opentrons": "Share robot logs", + "share_logs_with_opentrons_description": "Help improve this product by automatically sending anonymous robot logs. These logs are used to troubleshoot robot issues and spot error trends.", + "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the app. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact support for assistance.", + "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "update_requires_restarting_app": "Updating requires restarting the app.", + "update_robot_software_description": "Bypass the auto-update process and update the robot software manually.", + "update_robot_software_link": "Launch software update page", + "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "versions_sync": "Learn more about keeping the app and robot software in sync", + "want_to_help_out": "Want to help out?", + "welcome_title": "Welcome!", + "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Don't use Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." +} diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index 4a00283f3de6..389854a8b339 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,6 +1,5 @@ { "__dev_internal__protocolStats": "Protocol Stats", - "__dev_internal__enableRunTimeParameters": "Enable Run Time Parameters", "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "__dev_internal__enableQuickTransfer": "Enable Quick Transfer", "add_folder_button": "Add labware source folder", @@ -12,16 +11,12 @@ "additional_folder_location": "Additional Source Folder", "additional_labware_folder_title": "Additional Custom Labware Source Folder", "advanced": "Advanced", - "allow_sending_all_protocols_to_ot3": "Allow Sending All Protocols to Opentrons Flex", - "allow_sending_all_protocols_to_ot3_description": "Enable the \"Send to Opentrons Flex\" menu item for each imported protocol, even if protocol analysis fails or does not recognize it as designed for the Opentrons Flex.", - "analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", "app_changes": "App Changes in ", "app_settings": "App Settings", "bug_fixes": "Bug Fixes", "cal_block": "Always use calibration block to calibrate", "change_folder_button": "Change labware source folder", "channel": "Channel", - "choose_what_data_to_share": "Choose what data to share with Opentrons.", "clear_confirm": "Clear unavailable robots", "clear_robots_button": "Clear unavailable robots list", "clear_robots_description": "Clear the list of unavailable robots on the Devices page. This action cannot be undone.", @@ -36,7 +31,6 @@ "download_update": "Downloading update...", "enable_dev_tools": "Developer Tools", "enable_dev_tools_description": "Enabling this setting opens Developer Tools on app launch, enables additional logging and gives access to feature flags.", - "error_boundary_description": "You need to restart the touchscreen. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", "error_boundary_desktop_app_description": "You need to reload the app. Contact support with the following error message:", "error_boundary_title": "An unknown error has occurred", "feature_flags": "Feature Flags", @@ -47,20 +41,12 @@ "installing_update": "Installing update...", "ip_available": "Available", "ip_description_first": "Enter an IP address or hostname to connect to a robot.", - "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", - "learn_uninstalling": "Learn more about uninstalling the Opentrons App", "manage_versions": "It is very important for the robot and app software to be on the same version. Manage the robot software versions via Robot Settings > Advanced.", "new_features": "New Features", "no_folder": "No additional source folder specified", "no_specified_folder": "No path specified", "no_unavail_robots_to_clear": "No unavailable robots to clear", "not_found": "Not Found", - "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", - "opentrons_app_update": "Opentrons App update", - "opentrons_app_update_available": "Opentrons App Update Available", - "opentrons_app_update_available_variation": "An Opentrons App update is available.", - "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", - "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", "opt_in": "Opt in", "opt_in_description": "Automatically send us anonymous diagnostics and usage data. We only use this information to improve our products.", "opt_out": "Opt out", @@ -69,29 +55,22 @@ "override_path_to_python": "Override Path to Python", "prevent_robot_caching": "Prevent Robot Caching", "prevent_robot_caching_description": "The app will immediately clear unavailable robots and will not remember unavailable robots while this is enabled. On networks with many robots, preventing caching may improve network performance at the expense of slower and less reliable robot discovery on app launch.", - "previous_releases": "View previous Opentrons releases", "privacy": "Privacy", "problem_during_update": "This update is taking longer than usual.", "prompt": "Always show the prompt to choose calibration block or trash bin", - "receive_alert": "Receive an alert when an Opentrons software update is available.", "release_notes": "Release notes", "reload_app": "Reload app", "remind_later": "Remind me later", "reset_to_default": "Reset to default", "restart_touchscreen": "Restart touchscreen", "restarting_app": "Download complete, restarting the app...", - "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", "restore_previous": "See how to restore a previous software version", "searching": "Searching for 30s", "setup_connection": "Set up connection", - "share_app_analytics": "Share App Analytics with Opentrons", - "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", "share_display_usage": "Share display usage", - "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", "share_robot_logs": "Share robot logs", "share_robot_logs_description": "Data on actions the robot does, like running protocols.", "show_labware_offset_snippets": "Show Labware Offset data code snippets", - "show_labware_offset_snippets_description": "Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", "software_update_available": "Software Update Available", "software_version": "App Software Version", "successfully_deleted_unavail_robots": "Successfully deleted unavailable robots", @@ -105,7 +84,6 @@ "update_available": "Update available", "update_channel": "Update Channel", "update_description": "Stable receives the latest stable releases. Beta allows you to try out new in-progress features before they launch in Stable channel, but they have not completed testing yet.", - "update_requires_restarting": "Updating requires restarting the Opentrons App.", "usb_to_ethernet_adapter_description": "Description", "usb_to_ethernet_adapter_driver_version": "Driver Version", "usb_to_ethernet_adapter_info": "USB-to-Ethernet Adapter Information", @@ -117,11 +95,6 @@ "usb_to_ethernet_not_connected": "No USB-to-Ethernet adapter connected", "usb_to_ethernet_unknown_manufacturer": "Unknown Manufacturer", "usb_to_ethernet_unknown_product": "Unknown Adapter", - "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", - "view_change_log": "View Opentrons technical change log", - "view_issue_tracker": "View Opentrons issue tracker", - "view_release_notes": "View full Opentrons release notes", "view_software_update": "View software update", - "view_update": "View Update", - "want_to_help_out": "Want to help out Opentrons?" + "view_update": "View Update" } diff --git a/app/src/assets/localization/en/branded.json b/app/src/assets/localization/en/branded.json new file mode 100644 index 000000000000..13b53967aff6 --- /dev/null +++ b/app/src/assets/localization/en/branded.json @@ -0,0 +1,73 @@ +{ + "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", + "about_flex_gripper": "About Flex Gripper", + "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", + "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", + "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", + "choose_what_data_to_share": "Choose what data to share with Opentrons.", + "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", + "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", + "connect_and_screw_in_gripper": "Connect and secure Flex Gripper", + "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", + "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", + "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot, then try to reconnect.", + "contact_information": "Download the robot logs from the Opentrons App and send it to support@opentrons.com for assistance.", + "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", + "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, scan the QR code or search for “deck configuration” on support.opentrons.com", + "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.", + "error_boundary_description": "You need to restart the touchscreen. Then download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", + "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", + "find_your_robot": "Find your robot in the Opentrons App to install software updates.", + "firmware_update_download_logs": "Download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", + "general_error_message": "If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact Opentrons Support.", + "gripper_still_attached": "Flex Gripper still attached", + "gripper_successfully_attached_and_calibrated": "Flex Gripper successfully attached and calibrated", + "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", + "gripper_successfully_detached": "Flex Gripper successfully detached", + "gripper": "Flex Gripper", + "ip_description_second": "Opentrons recommends working with your network administrator to assign a static IP address to the robot.", + "learn_uninstalling": "Learn more about uninstalling the Opentrons App", + "loosen_screws_and_detach": "Loosen screws and detach Flex Gripper", + "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", + "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", + "module_calibration_get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", + "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", + "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", + "oem_mode_description": "Enable OEM Mode to remove all instances of Opentrons from the Flex touchscreen.", + "opentrons_app_successfully_updated": "The Opentrons App was successfully updated.", + "opentrons_app_update": "Opentrons App update", + "opentrons_app_update_available": "Opentrons App Update Available", + "opentrons_app_update_available_variation": "An Opentrons App update is available.", + "opentrons_app_will_use_interpreter": "If specified, the Opentrons App will use the Python interpreter at this path instead of the default bundled Python interpreter.", + "opentrons_cares_about_privacy": "Opentrons cares about your privacy. We anonymize all data and only use it to improve our products.", + "opentrons_def": "Opentrons Definition", + "opentrons_labware_def": "Opentrons labware definition", + "opentrons_tip_rack_name": "opentrons", + "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", + "previous_releases": "View previous Opentrons releases", + "receive_alert": "Receive an alert when an Opentrons software update is available.", + "restore_description": "Opentrons does not recommend reverting to previous software versions, but you can access previous releases below. For best results, uninstall the existing app and remove its configuration files before installing the previous version.", + "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", + "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", + "run_failed_modal_description_desktop": "Download the run log and send it to support@opentrons.com for assistance.", + "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front).", + "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", + "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", + "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", + "share_app_analytics": "Share App Analytics with Opentrons", + "share_app_analytics_description": "Help Opentrons improve its products and services by automatically sending anonymous diagnostics and usage data.", + "share_display_usage_description": "Data on how you interact with the touchscreen on Flex.", + "share_logs_with_opentrons": "Share Robot logs with Opentrons", + "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", + "show_labware_offset_snippets_description": "Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.", + "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", + "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", + "update_requires_restarting_app": "Updating requires restarting the Opentrons App.", + "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", + "update_robot_software_link": "Launch Opentrons software update page", + "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", + "versions_sync": "Learn more about keeping the Opentrons App and robot software in sync", + "want_to_help_out": "Want to help out Opentrons?", + "welcome_title": "Welcome to your Opentrons Flex!", + "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration." +} diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index d217718af42d..df0d7c743e27 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -1,11 +1,10 @@ { - "about_flex_gripper": "About Flex Gripper", "about_gripper": "About gripper", "about_module": "About {{name}}", "about_pipette_name": "About {{name}} Pipette", "about_pipette": "About pipette", - "add_fixture_description": "Add this fixture to your deck configuration. It will be referenced during protocol analysis.", - "add_to_slot_description": "Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.", + "add_fixture_description": "Add this item to your deck configuration. It will be referenced during protocol analysis.", + "add_to_slot_description": "Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.", "add_to_slot": "Add to slot {{slotName}}", "add": "Add", "an_error_occurred_while_updating_module": "An error occurred while updating your {{moduleName}}. Please try again.", @@ -40,8 +39,8 @@ "deck_configuration": "deck configuration", "deck_fixture_setup_instructions": "Deck fixture setup instructions", "deck_fixture_setup_modal_bottom_description_desktop": "For detailed instructions for different types of fixtures, scan the QR code or go to the link below.", - "deck_fixture_setup_modal_bottom_description": "For details on installing different fixture types, scan the QR code or search for “deck configuration” on support.opentrons.com", "deck_fixture_setup_modal_top_description": "First, unscrew and remove the deck slot where you'll install a fixture. Then put the fixture in place and attach it as needed.", + "deck_hardware": "deck hardware", "deck_slot": "deck slot {{slot}}", "delete_run": "Delete protocol run record", "detach_gripper": "Detach gripper", @@ -94,7 +93,6 @@ "module_calibration_required_update_pipette_FW": "Update pipette firmware before proceeding with required module calibration.", "module_calibration_required": "Module calibration required.", "module_controls": "Module Controls", - "module_error_contact_support": "Try powering the module off and on again. If the error persists, contact Opentrons Support.", "module_error": "Module error", "module_name_error": "{{moduleName}} error", "module_status_range": "Between {{min}} - {{max}} {{unit}}", @@ -177,7 +175,6 @@ "tempdeck_slideout_body": "Pre heat or cool your {{model}}. Enter a whole number between 4 °C and 96 °C.", "tempdeck_slideout_title": "Set Temperature for {{name}}", "temperature": "Temperature", - "these_are_advanced_settings": "These are advanced settings. Please do not attempt to adjust without assistance from Opentrons Support. Changing these settings may affect the lifespan of your pipette.These settings do not override any pipette settings defined in protocols.", "this_robot_will_restart_with_update": "This robot has to restart to update its software. Restarting will immediately stop the current run or calibration.Do you want to update now anyway?", "tip_pickup_drop": "Tip Pickup / Drop", "to_run_protocol_go_to_protocols_page": "To run a protocol on this robot, import a protocol on the Protocols page", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index f92a6e30d69c..3aec18d24a6b 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -6,7 +6,6 @@ "advanced": "Advanced", "alpha_description": "Warning: alpha releases are feature-complete but may contain significant bugs.", "alternative_security_types": "Alternative security types", - "alternative_security_types_description": "The Opentrons App supports connecting Flex to various enterprise access points. Connect via USB and finish setup in the app.", "app_change_in": "App Changes in {{version}}", "apply_historic_offsets": "Apply Labware Offsets", "are_you_sure_you_want_to_disconnect": "Are you sure you want to disconnect from {{ssid}}?", @@ -51,6 +50,7 @@ "clear_option_runs_history_subtext": "Clears information about past runs of all protocols.", "clear_option_tip_length_calibrations": "Clear tip length calibrations", "cancel_software_update": "Cancel software update", + "complete_and_restart_robot": "Complete and restart robot", "confirm_device_reset_description": "This will permanently delete all protocol, calibration, and other data. You’ll have to redo initial setup before using the robot again.", "confirm_device_reset_heading": "Are you sure you want to reset your device?", "connect": "Connect", @@ -59,16 +59,13 @@ "connect_via": "Connect via {{type}}", "connect_via_usb_description_1": "1. Connect the USB A-to-B cable to the robot’s USB-B port.", "connect_via_usb_description_2": "2. Connect the cable to an open USB port on your computer.", - "connect_via_usb_description_3": "3. Launch the Opentrons App on the computer to continue.", "connected": "Connected", "connected_network": "Connected Network", "connected_to_ssid": "Connected to {{ssid}}", "connected_via": "Connected via {{networkInterface}}", "connecting_to": "Connecting to {{ssid}}...", "connection_description_ethernet": "Connect to your lab's wired network.", - "connection_description_usb": "Connect directly to a computer (running the Opentrons App).", "connection_description_wifi": "Find a network in your lab or enter your own.", - "connection_lost_description": "The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.", "connection_to_robot_lost": "Connection to robot lost", "deck_calibration_description": "Calibrating the deck is required for new robots or after you relocate your robot. Recalibrating the deck will require you to also recalibrate pipette offsets.", "deck_calibration_missing": "Deck calibration missing", @@ -111,6 +108,7 @@ "enable_status_light": "Enable status light", "enable_status_light_description": "Turn on or off the strip of color LEDs on the front of the robot.", "engaged": "Engaged", + "enter_factory_password": "Enter factory password", "enter_network_name": "Enter network name", "enter_password": "Enter password", "estop": "E-stop", @@ -119,17 +117,16 @@ "estop_missing": "E-stop missing", "estop_missing_description": "Your E-stop could be damaged or detached. {{robotName}} lost its connection to the E-stop, so it canceled the protocol. Connect a functioning E-stop to continue.", "estop_pressed": "E-stop pressed", - "estop_pressed_description": "First, safely clear the deck of any labware or spills. Then, twist the E-stop button clockwise. Finally, have Flex move the gantry to its home position.", "ethernet": "Ethernet", "ethernet_connection_description": "Connect an Ethernet cable to the back of the robot and a network switch or hub.", "exit": "exit", + "factory_mode": "Factory Mode", "factory_reset": "Factory Reset", "factory_reset_description": "Resets all settings. You’ll have to redo initial setup before using the robot again.", "factory_reset_modal_description": "This data cannot be retrieved later.", "factory_resets_cannot_be_undone": "Factory resets cannot be undone.", "failed_to_connect_to_ssid": "Failed to connect to {{ssid}}", "feature_flags": "Feature Flags", - "find_your_robot": "Find your robot in the Opentrons App to install software updates.", "finish_setup": "Finish setup", "firmware_version": "Firmware Version", "fully_calibrate_before_checking_health": "Fully calibrate your robot before checking calibration health", @@ -146,6 +143,7 @@ "install_e_stop": "Install the E-stop", "installing_software": "Installing software...", "installing_update": "Installing update...", + "invalid_password": "Invalid password", "ip_address": "IP Address", "join_other_network": "Join other network", "join_other_network_error_message": "Must be 2–32 characters long", @@ -157,6 +155,7 @@ "launch_jupyter_notebook": "Launch Jupyter Notebook", "legacy_settings": "Legacy Settings", "mac_address": "MAC Address", + "manage_oem_settings": "Manage OEM settings", "minutes": "{{minute}} minutes", "missing_calibration": "Missing calibration", "model_and_serial": "Pipette Model and Serial", @@ -174,7 +173,6 @@ "need_another_security_type": "Need another security type?", "network_name": "Network Name", "network_settings": "Network Settings", - "network_setup_menu_description": "You’ll use this connection to run software updates and load protocols onto your Opentrons Flex.", "networking": "Networking", "never": "Never", "new_features": "New Features", @@ -193,7 +191,10 @@ "not_connected_via_wifi": "Not connected via Wi-Fi", "not_connected_via_wired_usb": "Not connected via wired USB", "not_now": "Not now", + "oem_mode": "OEM Mode", + "off": "Off", "one_hour": "1 hour", + "on": "On", "other_networks": "Other Networks", "password": "Password", "password_error_message": "Must be at least 8 characters", @@ -240,10 +241,8 @@ "robot_operating_update_available": "Robot Operating System Update Available", "robot_serial_number": "Robot Serial Number", "robot_server_version": "Robot Server Version", - "robot_server_version_ot3_description": "The Opentrons Flex software includes the robot server and the touchscreen display interface.", "robot_settings": "Robot Settings", "robot_settings_advanced_unknown": "Unknown", - "robot_software_update_required": "A robot software update is required to run protocols with this version of the Opentrons App.", "robot_successfully_connected": "Robot successfully connected to {{networkName}}.", "robot_system_version": "Robot System Version", "robot_system_version_available": "Robot System Version {{releaseVersion}} available", @@ -261,10 +260,7 @@ "select_authentication_method": "Select authentication method for your selected network.", "sending_software": "Sending software...", "serial": "Serial", - "share_logs_with_opentrons": "Share Robot logs with Opentrons", - "share_logs_with_opentrons_description": "Help Opentrons improve its products and services by automatically sending anonymous robot logs. Opentrons uses these logs to troubleshoot robot issues and spot error trends.", - "share_logs_with_opentrons_description_short": "Share anonymous robot logs with Opentrons.", - "share_logs_with_opentrons_short": "Share Robot logs", + "setup_mode": "Setup mode", "short_trash_bin": "Short trash bin", "short_trash_bin_description": "For pre-2019 robots with trash bins that are 55mm tall (instead of 77mm default)", "show": "Show", @@ -278,7 +274,6 @@ "successfully_connected": "Successfully connected!", "successfully_connected_to_network": "Successfully connected to {{ssid}}!", "supported_protocol_api_versions": "Supported Protocol API Versions", - "switch_to_usb_description": "If your network uses a different authentication method, connect to the Opentrons App and finish Wi-Fi setup there.", "text_size": "Text Size", "text_size_description": "Text on all screens will adjust to the size you choose below.", "tip_length_calibrations_history": "See all Tip Length Calibration history", @@ -296,27 +291,20 @@ "update_found": "Update found!", "update_robot_now": "Update robot now", "update_robot_software": "Update robot software manually with a local file (.zip)", - "update_robot_software_description": "Bypass the Opentrons App auto-update process and update the robot software manually.", - "update_robot_software_link": "Launch Opentrons software update page", "updating": "Updating", - "update_requires_restarting": "Updating the robot software requires restarting the robot", + "update_requires_restarting_robot": "Updating the robot software requires restarting the robot", "usage_settings": "Usage Settings", "usb": "USB", "usb_to_ethernet_description": "Looking for USB-to-Ethernet Adapter info?", "use_older_aspirate": "Use older aspirate behavior", "use_older_aspirate_description": "Aspirate with the less accurate volumetric calibrations that were used before version 3.7.0. Use this if you need consistency with pre-v3.7.0 results. This only affects GEN1 P10S, P10M, P50M, and P300S pipettes.", "use_older_protocol_analysis_method": "Use older protocol analysis method", - "use_older_protocol_analysis_method_description": "Use an older, slower method of analyzing uploaded protocols. This changes how the OT-2 validates your protocol during the upload step, but does not affect how your protocol actually runs. Opentrons Support might ask you to change this setting if you encounter problems with the newer, faster protocol analysis method.", "validating_software": "Validating software...", "view_details": "View details", "view_latest_release_notes_at": "View latest release notes at {{url}}", "view_network_details": "View network details", - "view_opentrons_issue_tracker": "View Opentrons issue tracker", - "view_opentrons_release_notes": "View full Opentrons release notes", - "view_opentrons_technical_change_log": "View Opentrons technical change log", "view_update": "View update", "welcome_description": "Quickly run protocols and check on your robot's status right on your lab bench.", - "welcome_title": "Welcome to your Opentrons Flex!", "wifi": "Wi-Fi", "wired_ip": "Wired IP", "wired_mac_address": "Wired MAC Address", diff --git a/app/src/assets/localization/en/devices_landing.json b/app/src/assets/localization/en/devices_landing.json index 60a25974fecd..b0a3307ace19 100644 --- a/app/src/assets/localization/en/devices_landing.json +++ b/app/src/assets/localization/en/devices_landing.json @@ -4,7 +4,6 @@ "check_same_network": "Check that the computer and robot are on the same network", "connect_to_network": "Connect to network", "connection_troubleshooting_intro": "If you’re having trouble with the robot’s connection, try these troubleshooting tasks. First, double check that the robot is powered on.", - "contact_support_for_connection_help": "If none of these work, contact Opentrons Support for help (via the question mark link in this app, or by emailing {{support_email}}.)", "deck_configuration": "Deck configuration", "devices": "Devices", "disconnect_from_network": "Disconnect from network", diff --git a/app/src/assets/localization/en/drop_tip_wizard.json b/app/src/assets/localization/en/drop_tip_wizard.json index 66924d002107..fc3bf25dfdfc 100644 --- a/app/src/assets/localization/en/drop_tip_wizard.json +++ b/app/src/assets/localization/en/drop_tip_wizard.json @@ -3,10 +3,12 @@ "begin_removal": "Begin removal", "blowout_complete": "blowout complete", "blowout_liquid": "Blow out liquid", + "cant_safely_drop_tips": "Can't safely drop tips", "choose_blowout_location": "choose blowout location", "choose_drop_tip_location": "choose tip-drop location", "confirm_blowout_location": "Is the pipette positioned where the liquids should be blown out?", "confirm_drop_tip_location": "Is the pipette positioned where the tips should be dropped?", + "confirm_removal_and_home": "Confirm removal and home", "drop_tip_complete": "tip drop complete", "drop_tip_failed": "The drop tip could not be completed. Contact customer support for assistance.", "drop_tips": "drop tips", @@ -21,6 +23,7 @@ "position_the_pipette": "position the pipette", "remove_the_tips": "You may want to remove the tips from the {{mount}} Pipette before using it again in a protocol.", "remove_the_tips_from_pipette": "You may want to remove the tips from the pipette before using it again in a protocol.", + "remove_the_tips_manually": "Remove the tips manually. Then home the gantry. Homing with tips attached could pull liquid into the pipette and damage it.", "remove_tips": "Remove tips", "select_blowout_slot": "You can blow out liquid into a labware or dispose of it.Select the slot where you want to blow out the liquid on the deck map to the right. Once confirmed, the gantry will move to the chosen slot.", "select_blowout_slot_odd": "You can blow out liquid into a labware or dispose of it.
After the gantry moves to the chosen slot, use the jog controls to move the pipette to the exact position for blowing out.", diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json new file mode 100644 index 000000000000..7531853df163 --- /dev/null +++ b/app/src/assets/localization/en/error_recovery.json @@ -0,0 +1,3 @@ +{ + "run_paused": "Run paused" +} diff --git a/app/src/assets/localization/en/firmware_update.json b/app/src/assets/localization/en/firmware_update.json index 8abe122d914e..5d543032e123 100644 --- a/app/src/assets/localization/en/firmware_update.json +++ b/app/src/assets/localization/en/firmware_update.json @@ -1,10 +1,10 @@ { - "download_logs": "Download the robot logs from the Opentrons App and send them to support@opentrons.com for assistance.", "firmware_out_of_date": "The firmware for {{mount}} {{instrument}} is out of date. You need to update it before running protocols that use this instrument.", "gantry_x": "Gantry X", "gantry_y": "Gantry Y", "gripper": "Gripper", "head": "Head", + "hepa_uv": "HEPA/UV Module", "pipette_left": "pipette", "pipette_right": "pipette", "ready_to_use": "Your {{instrument}} is ready to use!", diff --git a/app/src/assets/localization/en/gripper_wizard_flows.json b/app/src/assets/localization/en/gripper_wizard_flows.json index a868cdb474e3..ff98d8e07f0d 100644 --- a/app/src/assets/localization/en/gripper_wizard_flows.json +++ b/app/src/assets/localization/en/gripper_wizard_flows.json @@ -8,7 +8,6 @@ "calibration_pin": "Calibration Pin", "calibration_pin_touching": "The calibration pin will touch the calibration square in slot {{slot}} to determine its exact position.", "complete_calibration": "Complete calibration", - "connect_and_screw_in_gripper": "Connect and secure Flex Gripper", "continue": "Continue", "continue_calibration": "Continue calibration", "detach_gripper": "Detach Gripper", @@ -17,17 +16,11 @@ "get_started": "Get started", "gripper_calibration": "Gripper Calibration", "gripper_recalibration": "Gripper Recalibration", - "gripper_still_attached": "Flex Gripper still attached", - "gripper_successfully_attached_and_calibrated": "Flex Gripper successfully attached and calibrated", "gripper_successfully_attached": "Gripper successfully attached", - "gripper_successfully_calibrated": "Flex Gripper successfully calibrated", - "gripper_successfully_detached": "Flex Gripper successfully detached", - "gripper": "Flex Gripper", "hex_screwdriver": "2.5 mm Hex Screwdriver", "hold_gripper_and_loosen_screws": "Hold the gripper in place and loosen the top gripper screw first. After that move onto the bottom screw. (The screws are captive and will not come apart from the gripper.) Then carefully remove the gripper.", "insert_pin_into_front_jaw": "Insert calibration pin in front jaw", "insert_pin_into_rear_jaw": "Insert calibration pin in rear jaw", - "loosen_screws_and_detach": "Loosen screws and detach Flex Gripper", "move_gantry_to_front": "Move gantry to front", "move_pin_from_front_to_rear_jaw": "Remove the calibration pin from the front jaw and attach it to the rear jaw.", "move_pin_from_rear_jaw_to_storage": "Take the calibration pin from the rear gripper jaw and return it to its storage location.", diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts index c7256b1d415e..51acf92db530 100644 --- a/app/src/assets/localization/en/index.ts +++ b/app/src/assets/localization/en/index.ts @@ -1,5 +1,7 @@ import shared from './shared.json' +import anonymous from './anonymous.json' import app_settings from './app_settings.json' +import branded from './branded.json' import change_pipette from './change_pipette.json' import protocol_command_text from './protocol_command_text.json' import device_details from './device_details.json' @@ -14,25 +16,23 @@ import labware_details from './labware_details.json' import labware_landing from './labware_landing.json' import labware_position_check from './labware_position_check.json' import module_wizard_flows from './module_wizard_flows.json' -import more_network_and_system from './more_network_and_system.json' -import more_panel from './more_panel.json' import pipette_wizard_flows from './pipette_wizard_flows.json' -import protocol_calibration from './protocol_calibration.json' import protocol_details from './protocol_details.json' import protocol_info from './protocol_info.json' import protocol_list from './protocol_list.json' import protocol_setup from './protocol_setup.json' -import robot_advanced_settings from './robot_advanced_settings.json' +import quick_transfer from './quick_transfer.json' import robot_calibration from './robot_calibration.json' -import robot_connection from './robot_connection.json' import robot_controls from './robot_controls.json' -import robot_info from './robot_info.json' import run_details from './run_details.json' import top_navigation from './top_navigation.json' +import error_recovery from './error_recovery.json' export const en = { shared, + anonymous, app_settings, + branded, change_pipette, protocol_command_text, device_details, @@ -47,19 +47,15 @@ export const en = { labware_landing, labware_position_check, module_wizard_flows, - more_network_and_system, - more_panel, pipette_wizard_flows, - protocol_calibration, protocol_details, protocol_info, protocol_list, protocol_setup, - robot_advanced_settings, + quick_transfer, robot_calibration, - robot_connection, robot_controls, - robot_info, run_details, top_navigation, + error_recovery, } diff --git a/app/src/assets/localization/en/labware_landing.json b/app/src/assets/localization/en/labware_landing.json index 632125675617..17eebda979de 100644 --- a/app/src/assets/localization/en/labware_landing.json +++ b/app/src/assets/localization/en/labware_landing.json @@ -22,8 +22,6 @@ "labware": "labware", "last_updated": "Last updated", "open_labware_creator": "Open Labware Creator", - "opentrons_def": "Opentrons Definition", - "opentrons_labware_def": "Opentrons labware definition", "show_in_folder": "Show in folder", "unable_to_upload": "Unable to upload file", "yes_delete_def": "Yes, delete definition" diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index f08c465f7fa1..4072826650a5 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -27,9 +27,6 @@ "detach_probe": "Remove calibration probe", "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", "ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", - "error_modal_header": "Something went wrong", - "error_modal_problem_in_app": "There was an error performing Labware Position Check. Please restart the app. If the problem persists, please contact Opentrons Support", - "error_modal_problem_on_robot": "There was an error processing your request on the robot", "exit_screen_confirm_exit": "Exit and discard all labware offsets", "exit_screen_go_back": "Go back to labware position check", "exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.", diff --git a/app/src/assets/localization/en/module_wizard_flows.json b/app/src/assets/localization/en/module_wizard_flows.json index 636bb3686623..a502b0ef7976 100644 --- a/app/src/assets/localization/en/module_wizard_flows.json +++ b/app/src/assets/localization/en/module_wizard_flows.json @@ -21,12 +21,10 @@ "exit": "Exit", "firmware_up_to_date": "{{module}} firmware up to date.", "firmware_updated": "{{module}} firmware updated!", - "get_started": "To get started, remove labware from the deck and clean up the working area to make the calibration easier. Also gather the needed equipment shown to the right.The calibration adapter came with your module. The pipette probe came with your Flex pipette.", "install_adapter": "Place calibration adapter in {{module}}", "install_calibration_adapter": "Install calibration adapter", "location_occupied": "A {{fixture}} is currently specified here on the deck configuration", "module_calibrating": "Stand back, {{moduleName}} is calibrating", - "module_calibration_failed": "Module calibration was unsuccessful. Make sure the calibration adapter is fully seated on the module and try again. If you still have trouble, contact Opentrons Support.{{error}}", "module_calibration": "Module calibration", "module_secured": "The module must be fully secured in its caddy and secured in the deck slot.", "module_too_hot": "Module is too hot to proceed to module calibration", diff --git a/app/src/assets/localization/en/more_network_and_system.json b/app/src/assets/localization/en/more_network_and_system.json deleted file mode 100644 index a6fd1593b38d..000000000000 --- a/app/src/assets/localization/en/more_network_and_system.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Description", - "driver_version": "Driver Version", - "launch_realtek_adapter_drivers_site": "Launch Realtek Adapter Drivers Site", - "manufacturer": "Manufacturer", - "network_and_system_title": "Network & System", - "u2e_adapter_information": "USB-to-Ethernet Adapter Information", - "u2e_driver_update_alert": "Update available for Realtek USB-to-Ethernet adapter driver" -} diff --git a/app/src/assets/localization/en/more_panel.json b/app/src/assets/localization/en/more_panel.json deleted file mode 100644 index 63b594c1f37e..000000000000 --- a/app/src/assets/localization/en/more_panel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "app": "App", - "custom_labware": "Custom Labware", - "network_and_system": "Network & System", - "resources": "Resources" -} diff --git a/app/src/assets/localization/en/pipette_wizard_flows.json b/app/src/assets/localization/en/pipette_wizard_flows.json index 5d13142349cb..53ae23d07e2c 100644 --- a/app/src/assets/localization/en/pipette_wizard_flows.json +++ b/app/src/assets/localization/en/pipette_wizard_flows.json @@ -32,7 +32,7 @@ "detach_mount_attach_96": "Detach {{mount}} Pipette and Attach 96-Channel Pipette", "detach_mounting_plate_instructions": "Hold onto the plate so it does not fall. Then remove the pins on the plate from the slots on the gantry carriage.", "detach_next_pipette": "Detach next pipette", - "detach_pipette_to_attach_96": "Detach {{pipetteName}} and attach 96-Channel pipette", + "detach_pipette_to_attach_96": "Detach {{pipetteName}} and Attach 96-Channel pipette", "detach_pipette": "detach {{mount}} pipette", "detach_pipettes_attach_96": "Detach Pipettes and Attach 96-Channel Pipette", "detach_z_axis_screw_again": "detach the z-axis screw before attaching the 96-Channel Pipette.", @@ -77,7 +77,6 @@ "return_probe_error": "Return the calibration probe to its storage location before exiting. {{error}}", "single_mount_attached_error": "Single mount pipette is selected when this is the 96 channel flow", "single_or_8_channel": "{{single}} or {{eight}} pipette", - "something_seems_wrong": "There may be a problem with your pipette. Exit setup and contact Opentrons Support for assistance.", "stand_back": "Stand back, robot is in motion", "try_again": "try again", "unable_to_detect_probe": "Unable to detect calibration probe", diff --git a/app/src/assets/localization/en/protocol_calibration.json b/app/src/assets/localization/en/protocol_calibration.json deleted file mode 100644 index 1c65b64721f4..000000000000 --- a/app/src/assets/localization/en/protocol_calibration.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "cal_data_existing_data": "Existing data", - "cal_data_legacy_definition": "Calibration Data N/A", - "cal_data_not_calibrated": "Not yet calibrated", - "cal_data_updated_data": "Updated data", - "cal_panel_title": "Placement and Calibration", - "labware_cal_labware_title": "labware", - "labware_cal_tipracks_title": "tip racks", - "labware_cal_title": "Labware Calibration", - "module_connect_description": "Power up and plug in the required module(s) via the OT-2 USB Ports. Place the modules as shown in the deck map.", - "module_connect_duplicate_description": "Plug the modules in to the USB ports as listed in the Placement and Calibration Panel. Check out our help docs for more information on using modules of the same type.", - "module_connect_instruction": "Plug the modules in to the USB ports as listed below, from left to right.", - "module_connect_missing_tooltip": "Connect module(s) to proceed to labware calibration", - "module_connect_proceed_button": "Continue to labware setup", - "modules_deck_slot_title": "slot", - "modules_module_title": "module", - "modules_title": "modules", - "modules_update_software_tooltip": "Update robot software to see USB port information", - "modules_usb_order_title": "USB order (L to R)", - "modules_usb_port_title": "USB port", - "tip_length_cal_title": "Tip Length Calibration" -} diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index c5cc80f28046..fafd2c980383 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -39,6 +39,7 @@ "not_connected": "not connected", "not_in_protocol": "no {{section}} is specified for this protocol", "num_choices": "{{num}} choices", + "num_options": "{{num}} options", "off": "Off", "on_off": "On, off", "on": "On", @@ -80,6 +81,7 @@ "unavailable_robot_not_listed_plural": "{{count}} unavailable robots are not listed.", "unavailable_robot_not_listed": "{{count}} unavailable robot is not listed.", "unsuccessfully_sent": "Unsuccessfully sent", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_run_details": "View run details", "view_unavailable_robots": "View unavailable robots on the Devices page" } diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index bbaac1ce9c20..20d618db6018 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -10,7 +10,6 @@ "creation_method": "Creation Method", "custom_labware_not_supported": "Robot doesn't support custom labware", "date_added": "Date Added", - "delete_protocol_from_app": "Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.", "delete_protocol": "Delete protocol", "description": "Description", "drag_file_here": "Drag and drop protocol file here", @@ -65,19 +64,15 @@ "protocol_title": "Protocol - {{protocol_name}}", "protocol_upload_failed": "Protocol upload failed. Fix the error and try again", "protocols": "Protocols", + "quick_transfer": "Quick transfer", "required_cal_data_title": "Calibration Data", "required_quantity_title": "Quantity", "required_type_title": "Type", - "rerunning_protocol_modal_body": "Opentrons displays the connected robot’s last protocol run on on the Protocol Upload page. If you run again, Opentrons loads this protocol and applies Labware Offset data if any exists.Clicking “Run Again” will take you directly to the Run tab. If you’d like to review the deck setup or run Labware Position Check before running the protocol, navigate to the Protocol tab.If you recalibrate your robot, it will clear the last run from the upload page. A run can have the following statuses:Not started: when this protocol was loaded on to the robot, it was closed before the user ran itCanceled: when this protocol was loaded on to the robot, it was canceled before the run completedComplete: when this protocol was loaded on to the robot, it was closed after the protocol run completed", - "rerunning_protocol_modal_header": "How Rerunning A Protocol Works", - "rerunning_protocol_modal_link": "Learn more about Labware Offset Data", - "rerunning_protocol_modal_title": "See How Rerunning a Protocol Works", "robot_name_last_run": "{{robot_name}}’s last run", "robot_type_first": "{{robotType}} protocols first", "run_again": "Run again", "run_protocol": "Run protocol", "run_timestamp_title": "Run timestamp", - "send_a_protocol_to_store": "Send a protocol from the Opentrons App to get started.", "simulation_in_progress": "Simulation in Progress", "timestamp": "+{{index}}", "too_many_pins_body": "Remove a protocol in order to add more protocols to your pinned list.", diff --git a/app/src/assets/localization/en/protocol_list.json b/app/src/assets/localization/en/protocol_list.json index d014c27abaed..76c1b068d94e 100644 --- a/app/src/assets/localization/en/protocol_list.json +++ b/app/src/assets/localization/en/protocol_list.json @@ -1,5 +1,4 @@ { - "a_robot_software_update_is_available": "A robot software update is required to run protocols with this version of the Opentrons App. Go to Robot", "delete_protocol_message": " and its run history will be permanently deleted.", "last_updated_at": "Updated {{date}}", "left_mount": "left mount", diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 084debdb5f01..3bb871d4e640 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -38,6 +38,7 @@ "calibration_status": "calibration status", "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", + "choose_enum": "Choose {{displayName}}", "closing": "Closing...", "complete_setup_before_proceeding": "complete setup before continuing run", "configure": "Configure", @@ -59,6 +60,7 @@ "deck_conflict_info_thermocycler": "Update the deck configuration by removing the fixtures in locations A1 and B1. Either remove the fixtures from the deck configuration or update the protocol.", "deck_conflict_info": "Update the deck configuration by removing the {{currentFixture}} in location {{cutout}}. Either remove the fixture from the deck configuration or update the protocol.", "deck_conflict": "Deck location conflict", + "deck_hardware": "Deck hardware", "deck_map": "Deck Map", "default_values": "Default values", "example": "Example", @@ -120,8 +122,6 @@ "lpc_disabled_modules_not_connected": "Make sure all modules are connected before running Labware Position Check", "lpc_disabled_no_tipracks_loaded": "Labware Position Check requires that the protocol loads a tip rack", "lpc_disabled_no_tipracks_used": "Labware Position Check requires that the protocol has at least one pipette that picks up a tip", - "magnetic_module_attention_warning": "Opentrons recommends securing labware with the module’s bracket. See how to secure labware to the Magnetic Module", - "magnetic_module_extra_attention": "Opentrons recommends securing labware with the module’s bracket", "map_view": "Map View", "missing_gripper": "Missing gripper", "missing_instruments": "Missing {{count}}", @@ -129,7 +129,6 @@ "missing_pipettes": "Missing {{count}} pipette", "missing": "Missing", "modal_instructions_title": "{{moduleName}} Setup Instructions", - "modal_instructions": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box. You can also click the link below or scan the QR code to visit the modules section of the Opentrons Help Center.", "module_and_deck_setup": "Modules & deck", "module_connected": "Connected", "module_disconnected": "Disconnected", @@ -161,12 +160,14 @@ "must_have_labware_and_pip": "Protocol must load labware and a pipette", "n_a": "N/A", "name": "Name", + "no_custom_values": "No custom values specified", "no_data": "no data", "no_labware_offset_data": "no labware offset data yet", "no_modules_or_fixtures": "No modules or fixtures are specified for this protocol.", "no_modules_specified": "no modules are specified for this protocol.", "no_modules_used_in_this_protocol": "No hardware used in this protocol", "no_parameters_specified": "No parameters specified", + "no_parameters_specified_in_protocol": "No parameters specified in this protocol", "no_tiprack_loaded": "Protocol must load a tip rack", "no_tiprack_used": "Protocol must pick up a tip", "no_usb_connection_required": "No USB connection required", @@ -224,7 +225,8 @@ "resolve": "Resolve", "restart_setup_and_try": "Restart setup and try using different parameter values.", "restart_setup": "Restart setup", - "restore_default": "Restore default values", + "restore_defaults": "Restore default values", + "restore_default": "Restore default value", "robot_cal_description": "Robot calibration establishes how the robot knows where it is in relation to the deck. Accurate Robot calibration is essential to run protocols successfully. Robot calibration has 3 parts: Deck calibration, Tip Length calibration and Pipette Offset calibration.", "robot_cal_help_title": "How Robot Calibration Works", "robot_calibration_step_description_pipettes_only": "Review required instruments and calibrations for this protocol.", @@ -235,22 +237,16 @@ "run_disabled_modules_not_connected": "Make sure all modules are connected before proceeding to run", "run_labware_position_check": "run labware position check", "run": "Run", - "secure_labware_explanation_magnetic_module": "Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module. Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).", - "secure_labware_explanation_thermocycler": "Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.", "secure_labware_instructions": "Secure labware instructions", "secure_labware_modal": "Securing labware to the {{name}}", "secure": "Secure", "setup_for_run": "Setup for Run", - "setup_instructions_description": "For step-by-step instructions on setting up your module, consult the Quickstart Guide that came in its box or scan the QR code to visit the modules section of the Opentrons Help Center.", "setup_instructions": "setup instructions", "setup_is_view_only": "Setup is view-only once run has started", "slot_location": "Slot {{slotName}}", "slot_number": "Slot Number", "status": "Status", "step": "STEP {{index}}", - "thermocycler_attention_warning": " Labware must be secured with the module’s latch. See how to secure labware to the Thermocycler Module Thermocycler lid must be open when robot moves to the slots it occupies. Opentrons will automatically open the lid to move to these slots during Labware Position Check.", - "thermocycler_extra_attention_gen_1": "Labware must be secured with the module’s latch. Thermocycler lid must be open when robot moves to the slots it occupies. Opentrons will automatically open the lid to move to these slots during Labware Position Check.", - "thermocycler_extra_attention_gen_2": "The lid will automatically open when moving to these slots during Labware Position Check.", "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipette’s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.", "tip_length_cal_title": "Tip Length Calibration", @@ -260,13 +256,15 @@ "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", "usb_port_connected": "USB Port {{port}}", + "usb_port_number": "USB-{{port}}", "value": "Value", "values_are_view_only": "Values are view-only", + "value_out_of_range_generic": "Value must be in range", + "value_out_of_range": "Value must be between {{min}}-{{max}}", "view_current_offsets": "View current offsets", "view_moam": "View setup instructions for placing modules of the same type to the robot.", "view_setup_instructions": "View setup instructions", "volume": "Volume", "what_labware_offset_is": "A Labware Offset is a type of positional adjustment that accounts for small, real-world variances in the overall position of the labware on a robot’s deck. Labware Offset data is unique to a specific combination of labware definition, deck slot, and robot.", - "why_use_lpc": "Labware Position Check is intended to correct for minor variances. Opentrons does not recommend using Labware Position Check to compensate for large positional adjustments. Needing to set large labware offsets could indicate a problem with robot calibration.", "with_the_chosen_value": "With the chosen values, the following error occurred:" } diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json new file mode 100644 index 000000000000..b0e9e294dc46 --- /dev/null +++ b/app/src/assets/localization/en/quick_transfer.json @@ -0,0 +1,21 @@ +{ + "create_new_transfer": "Create new quick transfer", + "left_mount": "Left Mount", + "both_mounts": "Left + Right Mount", + "right_mount": "Right Mount", + "select_attached_pipette": "Select attached pipette", + "select_dest_labware": "Select destination labware", + "select_dest_wells": "Select destination wells", + "select_source_labware": "Select source labware", + "select_source_wells": "Select source wells", + "select_tip_rack": "Select tip rack", + "set_aspirate_volume": "Set aspirate volume", + "set_dispense_volume": "Set dispense volume", + "set_transfer_volume": "Set transfer volume", + "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.", + "tip_rack": "Tip rack", + "labware": "Labware", + "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.", + "well_selection": "Well selection", + "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well)." +} diff --git a/app/src/assets/localization/en/robot_advanced_settings.json b/app/src/assets/localization/en/robot_advanced_settings.json deleted file mode 100644 index 8ce165ecaa0e..000000000000 --- a/app/src/assets/localization/en/robot_advanced_settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "download_logs_button": "download", - "download_logs_description": "Access logs from this robot.", - "download_logs_label": "download logs", - "log_opt_out_explanation": "If your OT-2 is connected to the internet, Opentrons will collect logs from your robot to troubleshoot issues and identify error trends.", - "log_opt_out_heading": "Robot Logging", - "log_opt_out_instruction": "If you would like to disable log collection, please click "Opt out" below.", - "open_jupyter_description": "Open the Jupyter Notebook running on this OT-2 in your web browser. (Experimental feature! See documentation for more details.)", - "open_jupyter_label": "Jupyter Notebook", - "opt_in": "Sounds Good!", - "opt_out": "Opt Out", - "reset_button": "reset", - "reset_description": "Restore robot to factory configuration.", - "reset_label": "factory reset", - "title": "advanced settings", - "update_from_file_description": "If your app is unable to auto-download robot updates, you can download the robot update yourself and update your robot manually.", - "update_from_file_label": "Update robot software from file" -} diff --git a/app/src/assets/localization/en/robot_calibration.json b/app/src/assets/localization/en/robot_calibration.json index 9828ed706c42..1e96d8d1c246 100644 --- a/app/src/assets/localization/en/robot_calibration.json +++ b/app/src/assets/localization/en/robot_calibration.json @@ -11,13 +11,11 @@ "calibrate_z_axis_on_block": "Calibrate z-axis on block", "calibrate_z_axis_on_slot": "Calibrate z-axis in slot 5", "calibrate_z_axis_on_trash": "Calibrate z-axis on trash bin", - "calibration_block_description": "This block is a specially made tool that fits perfectly on your deck and helps with calibration.If you do not have a Calibration Block, please email support@opentrons.com so we can send you one. In your message, be sure to include your name, company or institution name, and shipping address. While you wait for the block to arrive, you can use the flat surface on the trash bin of your robot instead.", "calibration_complete": "Calibration complete", "calibration_dashboard": "Calibration Dashboard", "calibration_health_check": "Calibration Health Check", "calibration_health_check_intro_body": "Calibration Health Check diagnoses problems with Deck, Tip Length, and Pipette Offset Calibration.You will move the pipettes to various positions, which will be compared against your existing calibration data.If there is a large difference, you will be prompted to redo some or all of your calibrations.", "calibration_health_check_results": "Calibration Health Check Results", - "calibration_on_opentrons_tips_is_important": "It’s extremely important to perform this calibration using the Opentrons tips and tip racks specified above, as the robot determines accuracy based on the known measurements of these tips.", "calibration_recommended": "Calibration recommended", "calibration_status": "Calibration Status", "calibration_status_description": "For accurate and precise movement, calibrate the robot's deck, pipette offsets, and tip lengths.", @@ -84,8 +82,6 @@ "need_help": "Need help?", "no_pipette": "No pipette attached", "no_tip_length": "Calibrate your pipette to see saved tip length", - "opentrons": "opentrons", - "opentrons_tip_racks_recommended": "Opentrons tip racks are highly recommended. Accuracy cannot be guaranteed with other tip racks.", "pick_up_tip": "Pick up tip", "pipette_name_and_serial": "{{name}}, {{serial}}", "pipette_offset_calibration": "Pipette Offset Calibration", diff --git a/app/src/assets/localization/en/robot_connection.json b/app/src/assets/localization/en/robot_connection.json deleted file mode 100644 index 46d1874d51c6..000000000000 --- a/app/src/assets/localization/en/robot_connection.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "connect": "connect", - "connected_description": "Your app is currently connected to your robot via {{type}} at IP address {{ip}}", - "connection_label": "this robot is currently", - "connection_status_default": "idle", - "connection_status_disconnected": "unknown - connect to view status", - "connection_status_not_connectable": "not connectable", - "connection_title": "status", - "disconnect": "disconnect", - "disconnected_description": "Your app is trying to connect to your robot via {{type}} at IP address {{ip}}", - "failed_connection_heading": "Could not connect to robot", - "health_status_not_ok": "not responding correctly to requests", - "health_status_ok": "responding to requests", - "health_status_unreachable": "unreachable", - "internet_status_full": "Internet: The robot is connected to a network and has full access to the Internet.", - "internet_status_limited": "Internet: The robot is connected to a network, but it has no access to the Internet.", - "internet_status_none": "Internet: The robot is not connected to any network.", - "internet_status_portal": "Internet: The robot is behind a captive portal and cannot reach the full Internet.", - "internet_status_unknown": "Internet: Unknown", - "internet_status": "Internet: Unknown", - "ip": "{{type}} IP: {{ip}}", - "last_resort": "If your robot remains unresponsive, please reach out to our Support team.", - "mac": "{{type}} MAC Address: {{mac}}", - "no_server_message": "This OT-2 has been seen recently, but it is currently {{status}} at IP address {{ip}}.We recommend power-cycling your robot.", - "server_message": "Your OT-2's API server is {{status}} at IP address {{ip}}.We recommend the following troubleshooting steps:
  1. Power-cycle your robot
  2. If power-cycling does not work, please update your robot's software
    (Note: your robot's update server is still responding and should accept an update.)
", - "subnet": "{{type}} Subnet Mask: {{subnet}}", - "success_banner": "{{robot}} successfully connected", - "title": "connectivity", - "unresponsive_title": "Unable to establish connection with robot", - "wired": "wired", - "wireless": "wireless" -} diff --git a/app/src/assets/localization/en/robot_info.json b/app/src/assets/localization/en/robot_info.json deleted file mode 100644 index f608623ac4ca..000000000000 --- a/app/src/assets/localization/en/robot_info.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "api_version_min_max": "min: {{min}}, max: {{max}}", - "firmware_version": "firmware version", - "robot_name": "robot name", - "server_version": "server version", - "supported_api_versions": "supported protocol API versions", - "title": "information" -} diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 90a6977806e2..ed0fbbdc7e70 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -5,11 +5,12 @@ "anticipated": "Anticipated steps", "apply_stored_data": "Apply stored data", "apply_stored_labware_offset_data": "Apply stored Labware Offset data?", - "cancel_run_alert_info": "Doing so will terminate this run, drop any attached tips in the trash container and home your robot.", + "cancel_run_alert_info_flex": "Doing so will terminate this run and home your robot.", + "cancel_run_alert_info_ot2": "Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.", "cancel_run_and_restart": "Cancel the run and restart setup to edit", "cancel_run_modal_back": "No, go back", "cancel_run_modal_confirm": "Yes, cancel run", - "cancel_run_modal_heading": "Are you sure you want to cancel this run?", + "cancel_run_modal_heading": "Are you sure you want to cancel?", "cancel_run_module_info": "Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.", "cancel_run": "Cancel run", "canceling_run_dot": "canceling run...", @@ -22,7 +23,6 @@ "comment_step": "Comment", "comment": "Comment", "complete_protocol_to_download": "Complete the protocol to download the run log", - "contact_information": "Download the robot logs from the Opentrons App and send it to support@opentrons.com for assistance.", "current_step_pause_timer": "Timer", "current_step_pause": "Current Step - Paused by User", "current_step": "Current Step", @@ -101,8 +101,6 @@ "run_completed": "Run completed.", "run_cta_disabled": "Complete required steps on Protocol tab before starting the run", "run_failed_modal_body": "Error occurred when protocol was {{command}}", - "run_failed_modal_description_desktop": "Download the run log and send it to support@opentrons.com for assistance.", - "run_failed_modal_description": "Please contact support@opentrons.com with relevant information for assistance with troubleshooting.", "run_failed_modal_header": "{{errorName}}: {{errorCode}} at protocol step {{count}}", "run_failed_modal_title": "Run failed", "run_failed_splash": "Run failed", diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index 8c8bed0a5afc..fe1a9bb21e64 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -6,14 +6,13 @@ "before_you_begin": "Before you begin", "browse": "browse", "cancel": "cancel", + "change_protocol": "Change protocol", "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", "close": "close", - "computer_in_app_is_controlling_robot": "A computer with the Opentrons App is currently controlling this robot.", "confirm_placement": "Confirm placement", "confirm_position": "Confirm position", - "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", "confirm_values": "Confirm values", "confirm": "Confirm", "continue_activity": "Continue activity", @@ -34,7 +33,6 @@ "exit": "exit", "extension_mount": "extension mount", "flow_complete": "{{flowName}} complete!", - "general_error_message": "If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.", "get_started": "Get started", "github": "GitHub", "go_back": "Go back", diff --git a/app/src/atoms/Banner/Banner.stories.tsx b/app/src/atoms/Banner/Banner.stories.tsx index deea5d236b49..0f3d62100752 100644 --- a/app/src/atoms/Banner/Banner.stories.tsx +++ b/app/src/atoms/Banner/Banner.stories.tsx @@ -1,36 +1,41 @@ import * as React from 'react' import { StyledText, TYPOGRAPHY } from '@opentrons/components' import { Banner } from './index' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Banner', component: Banner, -} as Meta +} + +export default meta -const Template: Story> = args => ( - {'Banner component'} -) +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - title: 'title', - type: 'success', +export const Primary: Story = { + args: { + children: 'Banner component', + type: 'success', + }, } -export const OverriddenIcon = Template.bind({}) -OverriddenIcon.args = { - type: 'warning', - title: 'Alert with overridden icon', - icon: { name: 'ot-hot-to-touch' }, + +export const OverriddenIcon: Story = { + args: { + type: 'warning', + children: 'Banner component', + icon: { name: 'ot-hot-to-touch' }, + }, } -export const OverriddenExitIcon = Template.bind({}) -OverriddenExitIcon.args = { - type: 'informing', - title: 'Alert with overriden exit icon', - onCloseClick: () => console.log('close'), - closeButton: ( - - {'Exit'} - - ), + +export const OverriddenExitIcon: Story = { + args: { + type: 'informing', + children: 'Banner component', + onCloseClick: () => console.log('close'), + closeButton: ( + + {'Exit'} + + ), + }, } diff --git a/app/src/atoms/Banner/__tests__/Banner.test.tsx b/app/src/atoms/Banner/__tests__/Banner.test.tsx index 126740f0c4b5..f543ec98ec0b 100644 --- a/app/src/atoms/Banner/__tests__/Banner.test.tsx +++ b/app/src/atoms/Banner/__tests__/Banner.test.tsx @@ -1,10 +1,9 @@ import * as React from 'react' import { describe, it, vi, expect, beforeEach } from 'vitest' -import '@testing-library/jest-dom/vitest' -import { fireEvent } from '@testing-library/react' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { Banner } from '..' -import { renderWithProviders } from '../../../__testing-utils__' const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -21,60 +20,67 @@ describe('Banner', () => { children: 'TITLE', } }) + it('renders success banner', () => { - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_success') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_success') + screen.getByText('TITLE') }) + it('renders success banner with exit button and when click dismisses banner', () => { props = { type: 'success', children: 'TITLE', onCloseClick: vi.fn(), } - const { getByText, getByLabelText } = render(props) - getByText('TITLE') - const btn = getByLabelText('close_icon') + render(props) + screen.getByText('TITLE') + const btn = screen.getByLabelText('close_icon') fireEvent.click(btn) expect(props.onCloseClick).toHaveBeenCalled() }) + it('renders warning banner', () => { props = { type: 'warning', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_warning') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_warning') + screen.getByText('TITLE') }) + it('renders error banner', () => { props = { type: 'error', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_error') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_error') + screen.getByText('TITLE') }) + it('renders updating banner', () => { props = { type: 'updating', children: 'TITLE', } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_updating') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_updating') + screen.getByText('TITLE') }) + it('renders custom icon banner', () => { props = { type: 'warning', children: 'TITLE', icon: { name: 'ot-hot-to-touch' }, } - const { getByText, getByLabelText } = render(props) - getByLabelText('icon_warning') - getByText('TITLE') + render(props) + screen.getByLabelText('icon_warning') + screen.getByText('TITLE') }) + it('renders custom close', () => { props = { type: 'warning', @@ -82,8 +88,8 @@ describe('Banner', () => { closeButton: 'close button', onCloseClick: vi.fn(), } - const { getByText } = render(props) - const btn = getByText('close button') + render(props) + const btn = screen.getByText('close button') fireEvent.click(btn) expect(props.onCloseClick).toHaveBeenCalled() }) diff --git a/app/src/atoms/Banner/index.tsx b/app/src/atoms/Banner/index.tsx index a74fcf829ba7..e7d2008521ad 100644 --- a/app/src/atoms/Banner/index.tsx +++ b/app/src/atoms/Banner/index.tsx @@ -8,13 +8,12 @@ import { DIRECTION_ROW, Flex, Icon, - IconProps, JUSTIFY_SPACE_BETWEEN, RESPONSIVENESS, SPACING, TYPOGRAPHY, } from '@opentrons/components' -import type { StyleProps } from '@opentrons/components' +import type { IconProps, StyleProps } from '@opentrons/components' export type BannerType = | 'success' diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index c1ff5fbeddd2..9be59bf19034 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -101,15 +101,16 @@ function Input(props: InputFieldProps): JSX.Element { tooltipText, ...inputProps } = props - const error = props.error != null + const hasError = props.error != null const value = props.isIndeterminate ?? false ? '' : props.value ?? '' const placeHolder = props.isIndeterminate ?? false ? '-' : props.placeholder const [targetProps, tooltipProps] = useHoverTooltip() const OUTER_CSS = css` @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing8}; &:focus-within { - filter: ${error + filter: ${hasError ? 'none' : `drop-shadow(0px 0px 10px ${COLORS.blue50})`}; } @@ -121,7 +122,7 @@ function Input(props: InputFieldProps): JSX.Element { background-color: ${COLORS.white}; border-radius: ${BORDERS.borderRadius4}; padding: ${SPACING.spacing8}; - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; + border: 1px ${BORDERS.styleSolid} ${hasError ? COLORS.red50 : COLORS.grey50}; font-size: ${TYPOGRAPHY.fontSizeP}; width: 100%; height: 2rem; @@ -144,17 +145,20 @@ function Input(props: InputFieldProps): JSX.Element { } &:hover { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey60}; } &:focus-visible { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey60}; outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; outline-offset: 3px; } &:focus-within { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.blue50}; + border: 1px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.blue50}; } &:disabled { @@ -168,15 +172,16 @@ function Input(props: InputFieldProps): JSX.Element { @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { height: ${size === 'small' ? '4.25rem' : '5rem'}; - box-shadow: ${error ? BORDERS.shadowBig : 'none'}; + box-shadow: ${hasError ? BORDERS.shadowBig : 'none'}; font-size: ${TYPOGRAPHY.fontSize28}; padding: ${SPACING.spacing16} ${SPACING.spacing24}; - border: 2px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; + border: 2px ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.grey50}; &:focus-within { box-shadow: none; - border: ${error ? '2px' : '3px'} ${BORDERS.styleSolid} - ${error ? COLORS.red50 : COLORS.blue50}; + border: ${hasError ? '2px' : '3px'} ${BORDERS.styleSolid} + ${hasError ? COLORS.red50 : COLORS.blue50}; } & input { @@ -191,19 +196,17 @@ function Input(props: InputFieldProps): JSX.Element { ` const FORM_BOTTOM_SPACE_STYLE = css` - padding: ${SPACING.spacing4} 0rem; + padding-top: ${SPACING.spacing4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + padding: ${SPACING.spacing8} 0rem; padding-bottom: 0; } ` const TITLE_STYLE = css` - color: ${error ? COLORS.red50 : COLORS.black90}; + color: ${hasError ? COLORS.red50 : COLORS.black90}; padding-bottom: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeLabel}; - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - line-height: ${TYPOGRAPHY.lineHeight12}; - align-text: ${textAlign}; + text-align: ${textAlign}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { font-size: ${TYPOGRAPHY.fontSize22}; font-weight: ${TYPOGRAPHY.fontWeightRegular}; @@ -214,9 +217,11 @@ function Input(props: InputFieldProps): JSX.Element { const ERROR_TEXT_STYLE = css` color: ${COLORS.red50}; + padding-top: ${SPACING.spacing4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { font-size: ${TYPOGRAPHY.fontSize22}; color: ${COLORS.red50}; + padding-top: ${SPACING.spacing8}; } ` @@ -239,9 +244,14 @@ function Input(props: InputFieldProps): JSX.Element { {title != null ? ( - + {title} - + {tooltipText != null ? ( <> @@ -277,16 +287,6 @@ function Input(props: InputFieldProps): JSX.Element { {props.units} ) : null} - {props.error != null ? ( - - {props.error} - - ) : null} {props.caption != null ? ( ) : null} + {hasError ? ( + + {props.error} + + ) : null} ) } diff --git a/app/src/atoms/Link/ExternalLink.stories.tsx b/app/src/atoms/Link/ExternalLink.stories.tsx index c243304ee592..8f664d257f51 100644 --- a/app/src/atoms/Link/ExternalLink.stories.tsx +++ b/app/src/atoms/Link/ExternalLink.stories.tsx @@ -1,22 +1,26 @@ import * as React from 'react' -import { Flex, COLORS } from '@opentrons/components' -import { ExternalLink } from './ExternalLink' +import { COLORS, Flex, SPACING } from '@opentrons/components' +import { ExternalLink as ExternalLinkComponent } from './ExternalLink' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/ExternalLink', - component: ExternalLink, -} as Meta - -const Template: Story> = args => ( - - - -) + component: ExternalLinkComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - href: 'https://www.opentrons.com', - children: 'Open the link', +export const ExternalLink: Story = { + args: { + href: 'https://www.opentrons.com', + children: 'Open the link', + }, } diff --git a/app/src/atoms/Link/ExternalLink.tsx b/app/src/atoms/Link/ExternalLink.tsx index 4baa78afa445..e35e3515277d 100644 --- a/app/src/atoms/Link/ExternalLink.tsx +++ b/app/src/atoms/Link/ExternalLink.tsx @@ -1,12 +1,7 @@ import * as React from 'react' -import { - Link, - LinkProps, - Icon, - TYPOGRAPHY, - SPACING, -} from '@opentrons/components' +import { Link, Icon, TYPOGRAPHY, SPACING } from '@opentrons/components' +import type { LinkProps } from '@opentrons/components' export interface ExternalLinkProps extends LinkProps { href: string diff --git a/app/src/atoms/MenuList/DropdownMenu.tsx b/app/src/atoms/MenuList/DropdownMenu.tsx index 7eafba80ecb2..ec383bf0ead6 100644 --- a/app/src/atoms/MenuList/DropdownMenu.tsx +++ b/app/src/atoms/MenuList/DropdownMenu.tsx @@ -15,7 +15,9 @@ import { TYPOGRAPHY, useOnClickOutside, POSITION_RELATIVE, + useHoverTooltip, } from '@opentrons/components' +import { Tooltip } from '../Tooltip' import { MenuItem } from './MenuItem' export interface DropdownOption { @@ -33,6 +35,7 @@ export interface DropdownMenuProps { dropdownType?: DropdownBorder title?: string caption?: string | null + tooltipText?: string } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility @@ -46,7 +49,9 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { dropdownType = 'rounded', title, caption, + tooltipText, } = props + const [targetProps, tooltipProps] = useHoverTooltip() const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) const toggleSetShowDropdownMenu = (): void => { setShowDropdownMenu(!showDropdownMenu) @@ -96,13 +101,27 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { return ( {title !== null ? ( - - {title} - + + + {title} + + {tooltipText != null ? ( + <> + + + + {tooltipText} + + ) : null} + ) : null} void - currentStep: number - maxSteps: number - // isExpanded is for collapse and expand animation - isExpanded?: boolean - footer?: React.ReactNode -} +import type { MultiSlideoutSpecs, SlideoutProps } from './index' + +type MultiSlideoutProps = SlideoutProps & MultiSlideoutSpecs export const MultiSlideout = (props: MultiSlideoutProps): JSX.Element => { const { diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx index 6d30005ad9ec..a610d352caf5 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/AlphanumericKeyboard.stories.tsx @@ -8,20 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { AlphanumericKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/AlphanumericKeyboard', component: AlphanumericKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story< - React.ComponentProps -> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -32,12 +32,14 @@ const Template: Story< value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -47,4 +49,6 @@ const Template: Story< ) } -export const AlphanumericSoftwareKeyboard = Template.bind({}) +export const AlphanumericSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css index 8816853e595c..1fa59e2230a2 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.css @@ -32,8 +32,7 @@ background-color: #dedede; /* grey30 */ } -.hg-layout-default .hg-row .hg-button, -.hg-layout-shift .hg-row .hg-button { +.alphanumericKeyboard .hg-button { height: 62.3px; } diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index af02f09b31f1..dccad085c08d 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,19 +1,25 @@ import * as React from 'react' import Keyboard from 'react-simple-keyboard' import { alphanumericKeyboardLayout, customDisplay } from '../constants' +import type { KeyboardReactInterface } from 'react-simple-keyboard' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface AlphanumericKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject + debug?: boolean } export function AlphanumericKeyboard({ onChange, keyboardRef, + debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const onKeyPress = (button: string): void => { - console.log(button) if (button === '{ABC}') handleShift() if (button === '{numbers}') handleNumber() if (button === '{abc}') handleUnShift() @@ -36,16 +42,16 @@ export function AlphanumericKeyboard({ return ( (keyboardRef.current = r)} - theme={'hg-theme-default oddTheme1'} + theme={'hg-theme-default oddTheme1 alphanumericKeyboard'} onChange={onChange} onKeyPress={onKeyPress} layoutName={layoutName} layout={alphanumericKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx index 3aaea8cb33d1..417c922876df 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/FullKeyboard.stories.tsx @@ -9,18 +9,18 @@ import { import { InputField } from '../../InputField' import { FullKeyboard } from '.' -import '../index.css' -import './index.css' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/FullKeyboard', component: FullKeyboard, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} +export default meta -const Template: Story> = args => { +type Story = StoryObj + +const Keyboard = (): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -31,12 +31,14 @@ const Template: Story> = args => { value={value} type="text" placeholder="When focusing, the keyboard shows up" + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression onFocus={() => setShowKeyboard(true)} /> {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} /> @@ -46,4 +48,6 @@ const Template: Story> = args => { ) } -export const FullSoftwareKeyboard = Template.bind({}) +export const FullSoftwareKeyboard: Story = { + render: () => , +} diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css index b54cde35e04a..b3ff8968da44 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.css @@ -24,10 +24,6 @@ grid-gap: 3px; } -.simple-keyboard.simple-keyboard.oddTheme1 .hg-button { - height: 44.75px; -} - .simple-keyboard.simple-keyboard.oddTheme1 .hg-button:not(:last-child) { margin-bottom: 3px; } @@ -42,7 +38,6 @@ .hg-layout-symbols .hg-row .hg-button, .hg-layout-numbers .hg-row .hg-button { color: #16212d; - height: 44.75px; font-size: 20px; font-style: normal; font-weight: 600; diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index 850ad689758f..663efdd9c242 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,15 +1,22 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { customDisplay, fullKeyboardLayout } from '../constants' +import type { KeyboardReactInterface } from 'react-simple-keyboard' +import '../index.css' +import './index.css' + +// TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject + debug?: boolean } export function FullKeyboard({ onChange, keyboardRef, + debug = false, }: FullKeyboardProps): JSX.Element { const [layoutName, setLayoutName] = React.useState('default') const handleShift = (button: string): void => { @@ -51,8 +58,9 @@ export function FullKeyboard({ layout={fullKeyboardLayout} display={customDisplay} mergeDisplay={true} - autoUseTouchEvents={true} useButtonTag={true} + debug={debug} // If true, will input a \n + baseClass="fullKeyboard" /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx index 3600dafc89a0..3f91df121f64 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/IndividualKey.stories.tsx @@ -8,18 +8,20 @@ import { } from '@opentrons/components' import { InputField } from '../../InputField' import { IndividualKey } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/IndividualKey', component: IndividualKey, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => { +const Keyboard = ({ ...args }): JSX.Element => { const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -49,7 +51,9 @@ const Template: Story> = args => { ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { - keyText: 'hello!', +export const IndividualKeySoftwareKeyboard: Story = args => ( + +) +IndividualKeySoftwareKeyboard.args = { + keyText: 'hello', } diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx index c501b0eccc6c..310008cddc8c 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/index.tsx @@ -1,19 +1,26 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' +import type { KeyboardReactInterface } from 'react-simple-keyboard' +import '../index.css' +import './index.css' const customDisplay = { '{backspace}': 'del', } + +// TODO (kk:04/05/2024) add debug to make debugging easy interface IndividualKeyProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject keyText: string + debug?: boolean } export function IndividualKey({ onChange, keyboardRef, keyText, + debug = false, }: IndividualKeyProps): JSX.Element { const numericalKeyboard = { layout: { @@ -31,10 +38,10 @@ export function IndividualKey({ onChange={onChange} layoutName="default" display={customDisplay} - autoUseTouchEvents={true} useButtonTag={true} {...numericalKeyboard} width="100%" + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx index 710750697ffe..53b3d714c4ca 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/NumericalKeyboard.stories.tsx @@ -4,40 +4,39 @@ import { Flex, POSITION_ABSOLUTE, SPACING, + VIEWPORT, } from '@opentrons/components' -import { touchScreenViewport } from '../../../DesignTokens/constants' import { InputField } from '../../InputField' import { NumericalKeyboard } from '.' -import '../index.css' -import './index.css' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/SoftwareKeyboard/NumericalKeyboard', component: NumericalKeyboard, - parameters: touchScreenViewport, + parameters: VIEWPORT.touchScreenViewport, argTypes: { isDecimal: { control: { type: 'boolean', options: [true, false], }, - defaultValue: false, }, hasHyphen: { control: { type: 'boolean', options: [true, false], }, - defaultValue: false, }, }, -} as Meta +} + +export default meta + +type Story = StoryObj -const Template: Story< - React.ComponentProps -> = args => { +const Keyboard = (args): JSX.Element => { + const { isDecimal, hasHyphen } = args const [showKeyboard, setShowKeyboard] = React.useState(false) const [value, setValue] = React.useState('') const keyboardRef = React.useRef(null) @@ -57,15 +56,15 @@ const Template: Story< position={POSITION_ABSOLUTE} top="20%" width="22.5rem" - height="21.25rem" + height="max-content" > {showKeyboard && ( e != null && setValue(String(e))} keyboardRef={keyboardRef} - isDecimal={args.isDecimal} - hasHyphen={args.hasHyphen} + isDecimal={isDecimal} + hasHyphen={hasHyphen} /> )} @@ -73,8 +72,8 @@ const Template: Story< ) } -export const Keyboard = Template.bind({}) -Keyboard.args = { +export const NumericalSoftwareKeyboard: Story = args => +NumericalSoftwareKeyboard.args = { isDecimal: false, hasHyphen: false, } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css index 239f86ba6647..28fe31599794 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.css @@ -21,12 +21,14 @@ .hg-layout-floatNegKeyboard .hg-row { grid-gap: 3px; } + +/* ToDo (kk:04/04/2024) this important will be removed when I refactor the entire css */ .numerical-keyboard .hg-row .hg-button { text-align: center; font-size: 20px; font-weight: 600; line-height: 24px; - height: 75px; + height: 75px !important; padding: 10px 22px; } diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx index 85d1a0b8b433..8c41120d5364 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/index.tsx @@ -1,12 +1,18 @@ import * as React from 'react' -import Keyboard from 'react-simple-keyboard' +import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { numericalKeyboardLayout, numericalCustom } from '../constants' +import type { KeyboardReactInterface } from 'react-simple-keyboard' +import '../index.css' +import './index.css' + +// Note (kk:04/05/2024) add debug to make debugging easy interface NumericalKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: React.MutableRefObject isDecimal?: boolean hasHyphen?: boolean + debug?: boolean } // the default keyboard layout intKeyboard that doesn't have decimal point and hyphen. @@ -15,6 +21,7 @@ export function NumericalKeyboard({ keyboardRef, isDecimal = false, hasHyphen = false, + debug = false, }: NumericalKeyboardProps): JSX.Element { const layoutName = `${isDecimal ? 'float' : 'int'}${ hasHyphen ? 'NegKeyboard' : 'Keyboard' @@ -30,10 +37,10 @@ export function NumericalKeyboard({ theme={'hg-theme-default oddTheme1 numerical-keyboard'} onChange={onChange} display={numericalCustom} - autoUseTouchEvents={true} useButtonTag={true} layoutName={layoutName} layout={numericalKeyboardLayout} + debug={debug} // If true, will input a \n /> ) } diff --git a/app/src/atoms/SoftwareKeyboard/index.css b/app/src/atoms/SoftwareKeyboard/index.css index 16fb1f9d25ff..f19179f4366d 100644 --- a/app/src/atoms/SoftwareKeyboard/index.css +++ b/app/src/atoms/SoftwareKeyboard/index.css @@ -4,7 +4,8 @@ background-color: #ececec; border-radius: 5px; box-sizing: border-box; - font-family: HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-family: HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, + Helvetica, Arial, Lucida Grande, sans-serif; overflow: hidden; padding: 5px; touch-action: manipulation; @@ -59,7 +60,6 @@ box-sizing: border-box; cursor: pointer; display: flex; - height: 40px; justify-content: center; padding: 5px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); @@ -81,15 +81,6 @@ width: 33.3%; } -.hg-theme-default .hg-button.hg-button-numpadadd, -.hg-theme-default .hg-button.hg-button-numpadenter { - height: 85px; -} - -.hg-theme-default .hg-button.hg-button-numpad0 { - width: 105px; -} - .hg-theme-default .hg-button.hg-button-com { max-width: 85px; } @@ -103,11 +94,11 @@ color: #fff; } -.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=".com"] { +.hg-theme-default .hg-button.hg-standardBtn[data-skbtn='.com'] { max-width: 82px; } -.hg-theme-default .hg-button.hg-standardBtn[data-skbtn="@"] { +.hg-theme-default .hg-button.hg-standardBtn[data-skbtn='@'] { max-width: 60px; } @@ -151,11 +142,11 @@ li.hg-candidate-box-list-item:active { } .hg-candidate-box-prev:before { - content: "◄"; + content: '◄'; } .hg-candidate-box-next:before { - content: "►"; + content: '►'; } .hg-candidate-box-next, @@ -179,4 +170,4 @@ li.hg-candidate-box-list-item:active { .hg-candidate-box-btn-active { color: #444; -} \ No newline at end of file +} diff --git a/app/src/atoms/buttons/LargeButton.stories.tsx b/app/src/atoms/buttons/LargeButton.stories.tsx index f1f9427a4cfb..d60e89d81f3a 100644 --- a/app/src/atoms/buttons/LargeButton.stories.tsx +++ b/app/src/atoms/buttons/LargeButton.stories.tsx @@ -1,35 +1,60 @@ -import * as React from 'react' -import { VIEWPORT } from '@opentrons/components' +import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { LargeButton } from './' -import type { Story, Meta } from '@storybook/react' -export default { +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { title: 'ODD/Atoms/Buttons/LargeButton', - argTypes: { onClick: { action: 'clicked' } }, + component: LargeButton, + argTypes: { + onClick: { action: 'clicked' }, + iconName: { + control: { + type: 'select', + }, + options: Object.keys(ICON_DATA_BY_NAME), + }, + }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const LargeButtonTemplate: Story< - React.ComponentProps -> = args => +type Story = StoryObj -export const PrimaryLargeButton = LargeButtonTemplate.bind({}) -PrimaryLargeButton.args = { - buttonText: 'Button text', - disabled: false, - iconName: 'play-round-corners', +export const Primary: Story = { + args: { + buttonText: 'Button text', + disabled: false, + iconName: 'play-round-corners', + }, +} +export const Secondary: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + iconName: 'build', + }, +} +export const Alert: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + iconName: 'reset', + }, } -export const SecondaryLargeButton = LargeButtonTemplate.bind({}) -SecondaryLargeButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, - iconName: 'build', +export const PrimaryNoIcon: Story = { + args: { + buttonText: 'Button text', + disabled: false, + }, } -export const AlertLargeButton = LargeButtonTemplate.bind({}) -AlertLargeButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, - iconName: 'reset', +export const PrimaryWithSubtext: Story = { + args: { + buttonText: 'Button text', + disabled: false, + subtext: 'Button subtext', + }, } diff --git a/app/src/atoms/buttons/LargeButton.tsx b/app/src/atoms/buttons/LargeButton.tsx index 6bfcf857d84d..c5e45d3b731a 100644 --- a/app/src/atoms/buttons/LargeButton.tsx +++ b/app/src/atoms/buttons/LargeButton.tsx @@ -7,6 +7,7 @@ import { DIRECTION_COLUMN, DISPLAY_FLEX, Icon, + Flex, JUSTIFY_SPACE_BETWEEN, SPACING, StyledText, @@ -20,7 +21,8 @@ interface LargeButtonProps extends StyleProps { onClick: () => void buttonType?: LargeButtonTypes buttonText: React.ReactNode - iconName: IconName + iconName?: IconName + subtext?: string disabled?: boolean } @@ -29,6 +31,7 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { buttonType = 'primary', buttonText, iconName, + subtext, disabled = false, ...buttonProps } = props @@ -110,23 +113,28 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { disabled={disabled} {...buttonProps} > - - {buttonText} - - + + + {buttonText} + + {subtext ? ( + + {subtext} + + ) : null} + + {iconName ? ( + + ) : null} ) } diff --git a/app/src/atoms/buttons/MediumButton.stories.tsx b/app/src/atoms/buttons/MediumButton.stories.tsx index 667947b7e08a..6c7fbd2fe5b7 100644 --- a/app/src/atoms/buttons/MediumButton.stories.tsx +++ b/app/src/atoms/buttons/MediumButton.stories.tsx @@ -1,73 +1,74 @@ -import * as React from 'react' import { ICON_DATA_BY_NAME, VIEWPORT } from '@opentrons/components' import { MediumButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/MediumButton', + component: MediumButton, argTypes: { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: undefined, + options: Object.keys(ICON_DATA_BY_NAME), }, buttonCategory: { control: { type: 'select', - options: ['default', 'rounded'], }, - defaultValue: undefined, + options: ['default', 'rounded'], }, onClick: { action: 'clicked' }, - width: { - control: { - type: 'text', - }, - defaultValue: undefined, - }, }, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} -const MediumButtonTemplate: Story< - React.ComponentProps -> = args => +export default meta +type Story = StoryObj -export const PrimaryMediumButton = MediumButtonTemplate.bind({}) -PrimaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'primary', - disabled: false, +export const PrimaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'primary', + disabled: false, + }, } -export const SecondaryMediumButton = MediumButtonTemplate.bind({}) -SecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'secondary', - disabled: false, + +export const SecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'secondary', + disabled: false, + }, } -export const AlertMediumButton = MediumButtonTemplate.bind({}) -AlertMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alert', - disabled: false, + +export const AlertMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alert', + disabled: false, + }, } -export const AlertSecondaryMediumButton = MediumButtonTemplate.bind({}) -AlertSecondaryMediumButton.args = { - buttonText: 'Button text', - buttonType: 'alertSecondary', - disabled: false, +export const AlertSecondaryMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'alertSecondary', + disabled: false, + }, } -export const TertiaryHighMediumButton = MediumButtonTemplate.bind({}) -TertiaryHighMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryHigh', - disabled: false, + +export const TertiaryHighMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryHigh', + disabled: false, + }, } -export const TertiaryLowLightMediumButton = MediumButtonTemplate.bind({}) -TertiaryLowLightMediumButton.args = { - buttonText: 'Button text', - buttonType: 'tertiaryLowLight', - disabled: false, + +export const TertiaryLowLightMediumButton: Story = { + args: { + buttonText: 'Button text', + buttonType: 'tertiaryLowLight', + disabled: false, + }, } diff --git a/app/src/atoms/buttons/SmallButton.stories.tsx b/app/src/atoms/buttons/SmallButton.stories.tsx index f587f7f4e13a..6566847a62c2 100644 --- a/app/src/atoms/buttons/SmallButton.stories.tsx +++ b/app/src/atoms/buttons/SmallButton.stories.tsx @@ -1,68 +1,75 @@ -import * as React from 'react' import { VIEWPORT } from '@opentrons/components' import { SmallButton } from './' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'ODD/Atoms/Buttons/SmallButton', argTypes: { onClick: { action: 'clicked' } }, component: SmallButton, parameters: VIEWPORT.touchScreenViewport, -} as Meta +} + +export default meta -const Template: Story> = args => ( - -) +type Story = StoryObj -export const Primary = Template.bind({}) -Primary.args = { - buttonText: 'Button text', +export const Primary: Story = { + args: { + buttonText: 'Button text', + }, } -export const Alert = Template.bind({}) -Alert.args = { - buttonType: 'alert', - buttonText: 'Button text', +export const Alert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + }, } -export const Secondary = Template.bind({}) -Secondary.args = { - buttonType: 'secondary', - buttonText: 'Button text', +export const Secondary: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + }, } -export const TertiaryLowLight = Template.bind({}) -TertiaryLowLight.args = { - buttonType: 'tertiaryLowLight', - buttonText: 'Button text', +export const TertiaryLowLight: Story = { + args: { + buttonType: 'tertiaryLowLight', + buttonText: 'Button text', + }, } -export const TertiaryHighLight = Template.bind({}) -TertiaryHighLight.args = { - buttonType: 'tertiaryHighLight', - buttonText: 'Button text', +export const TertiaryHighLight: Story = { + args: { + buttonType: 'tertiaryHighLight', + buttonText: 'Button text', + }, } -export const StartIconPrimary = Template.bind({}) -StartIconPrimary.args = { - buttonType: 'primary', - buttonText: 'Button text', - iconPlacement: 'startIcon', - iconName: 'reset', +export const StartIconPrimary: Story = { + args: { + buttonType: 'primary', + buttonText: 'Button text', + iconPlacement: 'startIcon', + iconName: 'reset', + }, } -export const EndIconAlert = Template.bind({}) -EndIconAlert.args = { - buttonType: 'alert', - buttonText: 'Button text', - iconPlacement: 'endIcon', - iconName: 'play-round-corners', +export const EndIconAlert: Story = { + args: { + buttonType: 'alert', + buttonText: 'Button text', + iconPlacement: 'endIcon', + iconName: 'play-round-corners', + }, } -export const SecondaryRounded = Template.bind({}) -SecondaryRounded.args = { - buttonType: 'secondary', - buttonText: 'Button text', - buttonCategory: 'rounded', +export const SecondaryRounded: Story = { + args: { + buttonType: 'secondary', + buttonText: 'Button text', + buttonCategory: 'rounded', + }, } diff --git a/app/src/atoms/structure/Divider.stories.tsx b/app/src/atoms/structure/Divider.stories.tsx index 301e40debf93..021eb5620200 100644 --- a/app/src/atoms/structure/Divider.stories.tsx +++ b/app/src/atoms/structure/Divider.stories.tsx @@ -8,49 +8,52 @@ import { StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { Divider } from './index' -import type { Story, Meta } from '@storybook/react' +import { Divider as DividerComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Divider', - component: Divider, -} as Meta + component: DividerComponent, + decorators: [ + Story => ( + <> + + + + + {'About Calibration'} + -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + {'Deck Calibration'} + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing16, + + ), + ], } +export default meta +type Story = StoryObj +export const Divider: Story = {} diff --git a/app/src/atoms/structure/Line.stories.tsx b/app/src/atoms/structure/Line.stories.tsx index ed017ed95e1f..46a90756c713 100644 --- a/app/src/atoms/structure/Line.stories.tsx +++ b/app/src/atoms/structure/Line.stories.tsx @@ -9,49 +9,57 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { Line } from './index' -import type { Story, Meta } from '@storybook/react' +import { Line as LineComponent } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'App/Atoms/Line', - component: Line, -} as Meta - -const Template: Story> = args => ( - <> - - - - - - {'About Calibration'} - - - - {'This section is about calibration.'} - + component: LineComponent, + decorators: [ + Story => ( + <> + + + + + + {'About Calibration'} + + + + {'This section is about calibration.'} + + + - - - - - - - - - {'Deck Calibration'} - - - - {'This section is for deck calibration.'} - + + + + + + + {'Deck Calibration'} + + + + {'This section is for deck calibration.'} + + + - - - -) - -export const Primary = Template.bind({}) -Primary.args = { - marginY: SPACING.spacing8, + + ), + ], } + +export default meta + +type Story = StoryObj + +export const Line: Story = {} diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 9e03af972c07..38bb6803b457 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -5,49 +5,43 @@ import { initReactI18next } from 'react-i18next' import { resources } from './assets/localization' import { titleCase } from '@opentrons/shared-data' -i18n.use(initReactI18next).init( - { - resources, - lng: 'en', - fallbackLng: 'en', - debug: process.env.NODE_ENV === 'development', - ns: [ - 'shared', - 'robot_advanced_settings', - 'robot_calibration', - 'robot_connection', - 'robot_controls', - 'robot_info', - 'top_navigation', - ], - defaultNS: 'shared', - interpolation: { - escapeValue: false, // not needed for react as it escapes by default - format: function (value, format, lng) { - if (format === 'upperCase') return value.toUpperCase() - if (format === 'lowerCase') return value.toLowerCase() - if (format === 'capitalize') return capitalize(value) - if (format === 'sentenceCase') return startCase(value) - if (format === 'titleCase') return titleCase(value) - return value - }, - }, - keySeparator: false, // use namespaces and context instead - saveMissing: true, - missingKeyHandler: (lng, ns, key) => { - process.env.NODE_ENV === 'test' - ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) - : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) +import type { InitOptions } from 'i18next' + +const i18nConfig: InitOptions = { + resources, + lng: 'en', + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + defaultNS: 'shared', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format, lng) { + if (format === 'upperCase') return value.toUpperCase() + if (format === 'lowerCase') return value.toLowerCase() + if (format === 'capitalize') return capitalize(value) + if (format === 'sentenceCase') return startCase(value) + if (format === 'titleCase') return titleCase(value) + return value }, }, - err => { - if (err) { - console.error( - 'Internationalization was not initialized properly. error: ', - err - ) - } + keySeparator: false, // use namespaces and context instead + saveMissing: true, + missingKeyHandler: (lng, ns, key) => { + process.env.NODE_ENV === 'test' + ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + }, +} + +const i18nCb = (err?: Error): void => { + if (err != null) { + console.error( + 'Internationalization was not initialized properly. error: ', + err + ) } -) +} + +void i18n.use(initReactI18next).init(i18nConfig, i18nCb) -export { i18n } +export { i18n, i18nCb, i18nConfig } diff --git a/app/src/index.tsx b/app/src/index.tsx index f6f4918d7695..e37435c9aba1 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -5,10 +5,8 @@ import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' -import { I18nextProvider } from 'react-i18next' import { ApiClientProvider } from '@opentrons/react-api-client' -import { i18n } from './i18n' import { createLogger } from './logger' import { uiInitialized } from './redux/shell' @@ -38,9 +36,7 @@ root.render( - - - + diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index b0f722b8c5a0..365c0a3eea50 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -111,9 +111,7 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { > {label} - - {description} - + {description} {menuOverlayItems != null && ( {source != null ? ( - renderer.processSync(source).contents + + {source} + ) : (

{DEFAULT_RELEASE_NOTES}

)} diff --git a/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx b/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx index 0ca2d39579fd..57331d825bfd 100644 --- a/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx +++ b/app/src/organisms/AdvancedSettings/OverridePathToPython.tsx @@ -30,7 +30,7 @@ import { import type { Dispatch } from '../../redux/types' export function OverridePathToPython(): JSX.Element { - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const pathToPythonInterpreter = useSelector(getPathToPythonOverride) const dispatch = useDispatch() const trackEvent = useTrackEvent() @@ -54,7 +54,7 @@ export function OverridePathToPython(): JSX.Element { {t('override_path_to_python')} - {t('opentrons_app_will_use_interpreter')} + {t('branded:opentrons_app_will_use_interpreter')} () const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn @@ -47,7 +47,7 @@ export function ShowLabwareOffsetSnippets(): JSX.Element { {t('show_labware_offset_snippets')} - {t('show_labware_offset_snippets_description')} + {t('branded:show_labware_offset_snippets_description')}
{ render() screen.getByText('Show Labware Offset data code snippets') screen.getByText( - 'Only for users who need to apply Labware Offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.' + 'Only for users who need to apply labware offset data outside of the Opentrons App. When enabled, code snippets for Jupyter Notebook and SSH are available during protocol setup.' ) screen.getByRole('switch', { name: 'show_link_to_get_labware_offset_data' }) }) diff --git a/app/src/organisms/Alerts/AlertsModal.tsx b/app/src/organisms/Alerts/AlertsModal.tsx index 928d5762a0ed..34b6f4c66c47 100644 --- a/app/src/organisms/Alerts/AlertsModal.tsx +++ b/app/src/organisms/Alerts/AlertsModal.tsx @@ -24,7 +24,7 @@ interface AlertsModalProps { export function AlertsModal({ toastIdRef }: AlertsModalProps): JSX.Element { const dispatch = useDispatch() const [showUpdateModal, setShowUpdateModal] = React.useState(false) - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const { makeToast } = useToaster() const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast() @@ -54,10 +54,14 @@ export function AlertsModal({ toastIdRef }: AlertsModalProps): JSX.Element { // Only run this hook on app startup React.useEffect(() => { if (hasJustUpdated) { - makeToast(t('opentrons_app_successfully_updated'), SUCCESS_TOAST, { - closeButton: true, - disableTimeout: true, - }) + makeToast( + t('branded:opentrons_app_successfully_updated'), + SUCCESS_TOAST, + { + closeButton: true, + disableTimeout: true, + } + ) dispatch(toggleConfigValue('update.hasJustUpdated')) } }, []) @@ -65,7 +69,7 @@ export function AlertsModal({ toastIdRef }: AlertsModalProps): JSX.Element { React.useEffect(() => { if (createAppUpdateAvailableToast) { toastIdRef.current = makeToast( - t('opentrons_app_update_available_variation'), + t('branded:opentrons_app_update_available_variation'), WARNING_TOAST, { closeButton: true, diff --git a/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx b/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx index eaef9184985b..1935cd33d78b 100644 --- a/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx +++ b/app/src/organisms/AppSettings/ConnectRobotSlideout.tsx @@ -43,7 +43,7 @@ export function ConnectRobotSlideout({ const [mostRecentDiscovered, setMostRecentDiscovered] = React.useState< boolean | null >(null) - const { t } = useTranslation(['app_settings', 'shared']) + const { t } = useTranslation(['app_settings', 'shared', 'branded']) const dispatch = useDispatch() const refreshDiscovery = (): unknown => dispatch(startDiscovery()) const isScanning = useSelector(getScanning) @@ -81,7 +81,7 @@ export function ConnectRobotSlideout({ {t('ip_description_first')} - {t('ip_description_second')} + {t('branded:ip_description_second')} - {t('restore_description')} + {t('branded:restore_description')} - {t('learn_uninstalling')} + {t('branded:learn_uninstalling')} - {t('previous_releases')} + {t('branded:previous_releases')} diff --git a/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx b/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx index 41a21ff0cba3..c767cb4ee394 100644 --- a/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx +++ b/app/src/organisms/CalibrateTipLength/AskForCalibrationBlockModal.tsx @@ -40,7 +40,7 @@ interface Props { } export function AskForCalibrationBlockModal(props: Props): JSX.Element { - const { t } = useTranslation(['robot_calibration', 'shared']) + const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) const [rememberPreference, setRememberPreference] = React.useState( true ) @@ -77,7 +77,7 @@ export function AskForCalibrationBlockModal(props: Props): JSX.Element { , supportLink: ( diff --git a/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx b/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx index fed5ab029116..08e0b22e51b6 100644 --- a/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx +++ b/app/src/organisms/CalibrationPanels/ChooseTipRack.tsx @@ -75,7 +75,7 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { robotName, defaultTipracks, } = props - const { t } = useTranslation(['robot_calibration', 'shared']) + const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) const pipSerial = usePipettesQuery( {}, { @@ -143,7 +143,7 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { customTipRacks.length > 0 ? [ { - label: t('opentrons'), + label: t('branded:opentrons_tip_rack_name'), options: opentronsTipRacksOptions, }, { @@ -233,14 +233,14 @@ export function ChooseTipRack(props: ChooseTipRackProps): JSX.Element { - {t('opentrons_tip_racks_recommended')} + {t('branded:opentrons_tip_racks_recommended')} - {t('calibration_on_opentrons_tips_is_important')} + {t('branded:calibration_on_opentrons_tips_is_important')} diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx index da15b3af90e8..cddbb2cd7a31 100644 --- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx +++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx @@ -15,6 +15,13 @@ const Template: Story> = args => ( export const Default = Template.bind({}) Default.args = { header: 'Header', + onClickBack: () => {}, +} + +export const TitleNoBackButton = Template.bind({}) +TitleNoBackButton.args = { + header: 'Header', + onClickBack: undefined, } export const TitleWithNormalSmallButton = Template.bind({}) @@ -22,6 +29,16 @@ TitleWithNormalSmallButton.args = { header: 'Header', buttonText: 'ButtonText', onClickButton: () => {}, + onClickBack: () => {}, +} + +export const TitleWithNormalSmallButtonDisabled = Template.bind({}) +TitleWithNormalSmallButtonDisabled.args = { + header: 'Header', + buttonText: 'ButtonText', + onClickButton: () => {}, + onClickBack: () => {}, + buttonIsDisabled: true, } export const TitleWithLinkButton = Template.bind({}) @@ -32,6 +49,7 @@ TitleWithLinkButton.args = { iconName: 'information', iconPlacement: 'startIcon', onClickButton: () => {}, + onClickBack: () => {}, } export const TitleWithTwoButtons = Template.bind({}) @@ -47,4 +65,5 @@ TitleWithTwoButtons.args = { buttonText: 'ButtonText', onClickButton: () => {}, secondaryButtonProps, + onClickBack: () => {}, } diff --git a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx index 8f53b640187f..8e2a1c7ec0e2 100644 --- a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx +++ b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx @@ -72,4 +72,26 @@ describe('ChildNavigation', () => { fireEvent.click(secondaryButton) expect(mockOnClickSecondaryButton).toHaveBeenCalled() }) + it.fails( + 'should not render back button if onClickBack does not exist', + () => { + props = { + ...props, + onClickBack: undefined, + } + render(props) + screen.getByTestId('ChildNavigation_Back_Button') + } + ) + it('should render button as disabled', () => { + props = { + ...props, + buttonText: 'mock button', + onClickButton: mockOnClickButton, + buttonIsDisabled: true, + } + render(props) + const button = screen.getByTestId('ChildNavigation_Primary_Button') + expect(button).toBeDisabled() + }) }) diff --git a/app/src/organisms/ChildNavigation/index.tsx b/app/src/organisms/ChildNavigation/index.tsx index afe3c1f75084..e076f7191af6 100644 --- a/app/src/organisms/ChildNavigation/index.tsx +++ b/app/src/organisms/ChildNavigation/index.tsx @@ -20,20 +20,21 @@ import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants' import { SmallButton } from '../../atoms/buttons' import { InlineNotification } from '../../atoms/InlineNotification' -import type { IconName } from '@opentrons/components' +import type { IconName, StyleProps } from '@opentrons/components' import type { InlineNotificationProps } from '../../atoms/InlineNotification' import type { IconPlacement, SmallButtonTypes, } from '../../atoms/buttons/SmallButton' -interface ChildNavigationProps { +interface ChildNavigationProps extends StyleProps { header: string - onClickBack: React.MouseEventHandler + onClickBack?: React.MouseEventHandler buttonText?: React.ReactNode inlineNotification?: InlineNotificationProps onClickButton?: React.MouseEventHandler buttonType?: SmallButtonTypes + buttonIsDisabled?: boolean iconName?: IconName iconPlacement?: IconPlacement secondaryButtonProps?: React.ComponentProps @@ -49,6 +50,8 @@ export function ChildNavigation({ iconName, iconPlacement, secondaryButtonProps, + buttonIsDisabled, + ...styleProps }: ChildNavigationProps): JSX.Element { return ( - - - + {onClickBack != null ? ( + + + + ) : null} {header} @@ -87,6 +93,8 @@ export function ChildNavigation({ onClick={onClickButton} iconName={iconName} iconPlacement={iconPlacement} + disabled={buttonIsDisabled} + data-testid="ChildNavigation_Primary_Button" /> ) : null} diff --git a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx index d5b910381bd7..11583264b3ec 100644 --- a/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/__tests__/ChooseProtocolSlideout.test.tsx @@ -58,6 +58,7 @@ describe('ChooseProtocolSlideout', () => { screen.getByText(/choose protocol to run/i) screen.getByText(/opentrons-robot-name/i) }) + it('renders an available protocol option for every stored protocol if any', () => { render({ robot: mockConnectableRobot, @@ -70,6 +71,7 @@ describe('ChooseProtocolSlideout', () => { screen.queryByRole('heading', { name: 'No protocols found' }) ).toBeNull() }) + it('renders an empty state if no protocol options', () => { vi.mocked(getStoredProtocols).mockReturnValue([]) render({ @@ -83,22 +85,55 @@ describe('ChooseProtocolSlideout', () => { screen.getByRole('heading', { name: 'No protocols found' }) ).toBeInTheDocument() }) - it('calls createRunFromProtocolSource if CTA clicked', () => { + + // it('calls createRunFromProtocolSource if CTA clicked', () => { + // const protocolDataWithoutRunTimeParameter = { + // ...storedProtocolDataFixture, + // runTimeParameters: [], + // } + // vi.mocked(getStoredProtocols).mockReturnValue([ + // protocolDataWithoutRunTimeParameter, + // ]) + // render({ + // robot: mockConnectableRobot, + // onCloseClick: vi.fn(), + // showSlideout: true, + // }) + // const proceedButton = screen.getByRole('button', { + // name: 'Proceed to setup', + // }) + // fireEvent.click(proceedButton) + // expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ + // files: [expect.any(File)], + // protocolKey: storedProtocolDataFixture.protocolKey, + // }) + // expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + // }) + + it('move to the second slideout if CTA clicked', () => { + const protocolDataWithoutRunTimeParameter = { + ...storedProtocolDataFixture, + runTimeParameters: [], + } + vi.mocked(getStoredProtocols).mockReturnValue([ + protocolDataWithoutRunTimeParameter, + ]) render({ robot: mockConnectableRobot, onCloseClick: vi.fn(), showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) - expect(mockCreateRunFromProtocol).toHaveBeenCalledWith({ - files: [expect.any(File)], - protocolKey: storedProtocolDataFixture.protocolKey, - }) - expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() + screen.getByText('Step 2 / 2') + screen.getByText('number of samples') + screen.getByText('Restore default values') }) + + // ToDo (kk:04/08) update test for RTP + /* it('renders error state when there is a run creation error', () => { vi.mocked(useCreateRunFromProtocol).mockReturnValue({ runCreationError: 'run creation error', @@ -153,4 +188,5 @@ describe('ChooseProtocolSlideout', () => { fireEvent.click(link) expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') }) + */ }) diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index b6d1d2805ff0..6f00082013af 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -2,7 +2,6 @@ import * as React from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' import { Link, NavLink, useHistory } from 'react-router-dom' -import { ApiHostProvider } from '@opentrons/react-api-client' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -12,30 +11,44 @@ import { Box, COLORS, DIRECTION_COLUMN, + DIRECTION_ROW, DISPLAY_BLOCK, Flex, Icon, + Link as LinkComponent, JUSTIFY_CENTER, + JUSTIFY_END, + JUSTIFY_FLEX_START, OVERFLOW_WRAP_ANYWHERE, PrimaryButton, ProtocolDeck, - SIZE_1, SPACING, + SecondaryButton, StyledText, TYPOGRAPHY, + useHoverTooltip, } from '@opentrons/components' +import { ApiHostProvider } from '@opentrons/react-api-client' import { useLogger } from '../../logger' import { OPENTRONS_USB } from '../../redux/discovery' import { getStoredProtocols } from '../../redux/protocol-storage' import { appShellRequestor } from '../../redux/shell/remote' -import { Slideout } from '../../atoms/Slideout' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' +import { Tooltip } from '../../atoms/Tooltip' +import { ToggleButton } from '../../atoms/buttons' +import { InputField } from '../../atoms/InputField' +import { DropdownMenu } from '../../atoms/MenuList/DropdownMenu' import { MiniCard } from '../../molecules/MiniCard' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { getAnalysisStatus } from '../ProtocolsLanding/utils' + +import type { RunTimeParameterCreateData } from '@opentrons/api-client' +import type { RunTimeParameter } from '@opentrons/shared-data' +import type { DropdownOption } from '@opentrons/components' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -65,6 +78,8 @@ export function ChooseProtocolSlideoutComponent( const { t } = useTranslation(['device_details', 'shared']) const history = useHistory() const logger = useLogger(new URL('', import.meta.url).pathname) + const [targetProps, tooltipProps] = useHoverTooltip() + const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -72,6 +87,27 @@ export function ChooseProtocolSlideoutComponent( selectedProtocol, setSelectedProtocol, ] = React.useState(null) + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState([]) + const [currentPage, setCurrentPage] = React.useState(1) + const [hasParamError, setHasParamError] = React.useState(false) + + React.useEffect(() => { + setRunTimeParametersOverrides( + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + ) + }, [selectedProtocol]) + React.useEffect(() => { + setHasParamError(errors.length > 0) + }, [runTimeParametersOverrides]) + + const runTimeParametersFromAnalysis = + selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] + + const hasRunTimeParameters = runTimeParametersFromAnalysis.length > 0 + const analysisStatus = getAnalysisStatus( false, selectedProtocol?.mostRecentAnalysis @@ -128,7 +164,14 @@ export function ChooseProtocolSlideoutComponent( location, definitionUri, })) - : [] + : [], + runTimeParametersOverrides.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) ) const handleProceed: React.MouseEventHandler = () => { if (selectedProtocol != null) { @@ -141,10 +184,240 @@ export function ChooseProtocolSlideoutComponent( logger.warn('failed to create protocol, no protocol selected') } } + + const isRestoreDefaultsLinkEnabled = + runTimeParametersOverrides?.some( + parameter => parameter.value !== parameter.default + ) ?? false + + const errors: string[] = [] + const runTimeParametersInputs = + runTimeParametersOverrides?.map((runtimeParam, index) => { + if ('choices' in runtimeParam) { + const dropdownOptions = runtimeParam.choices.map(choice => { + return { name: choice.displayName, value: choice.value } + }) as DropdownOption[] + return ( + { + return choice.value === runtimeParam.value + }) ?? dropdownOptions[0] + } + onClick={choice => { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + dropdownOptions.find(option => option.value === choice) + ?.value ?? parameter.default, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + title={runtimeParam.displayName} + width="100%" + dropdownType="neutral" + /> + ) + } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { + const value = runtimeParam.value as number + const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`protocol_details:value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + if (error != null) { + errors.push(error) + } + return ( + { + const clone = runTimeParametersOverrides.map((parameter, i) => { + if (i === index) { + return { + ...parameter, + value: + runtimeParam.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber, + } + } + return parameter + }) + setRunTimeParametersOverrides(clone) + }} + /> + ) + } else if (runtimeParam.type === 'bool') { + return ( + + + {runtimeParam.displayName} + + + { + const clone = runTimeParametersOverrides.map( + (parameter, i) => { + if (i === index) { + return { + ...parameter, + value: !parameter.value, + } + } + return parameter + } + ) + setRunTimeParametersOverrides(clone) + }} + height="0.813rem" + label={ + Boolean(runtimeParam.value) + ? t('protocol_details:on') + : t('protocol_details:off') + } + paddingTop={SPACING.spacing2} // manual alignment of SVG with value label + /> + + {Boolean(runtimeParam.value) + ? t('protocol_details:on') + : t('protocol_details:off')} + + + + {runtimeParam.description} + + + ) + } + }) ?? null + + const resetRunTimeParameters = (): void => { + setRunTimeParametersOverrides( + runTimeParametersOverrides?.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + ) + } + + const pageTwoBody = ( + + + + {t('protocol_details:restore_defaults')} + + {!isRestoreDefaultsLinkEnabled && ( + + {t('protocol_details:no_custom_values')} + + )} + + + {runTimeParametersInputs} + + + ) + + const singlePageFooter = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + + const multiPageFooter = + currentPage === 1 ? ( + setCurrentPage(2)} + width="100%" + disabled={isCreatingRun || selectedProtocol == null} + > + {t('shared:continue_to_param')} + + ) : ( + + setCurrentPage(1)} width="51%"> + {t('shared:change_protocol')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + return ( - { + onCloseClick() + setCurrentPage(1) + resetRunTimeParameters() + }} + currentStep={currentPage} + maxSteps={hasRunTimeParameters ? 2 : 1} title={t('choose_protocol_to_run', { name })} footer={ - - - {isCreatingRun ? ( - - ) : ( - t('shared:proceed_to_setup') - )} - + {currentPage === 1 ? ( + + ) : null} + {hasRunTimeParameters ? multiPageFooter : singlePageFooter} } > {showSlideout ? ( - { - if (!isCreatingRun) { - resetCreateRun() - setSelectedProtocol(storedProtocol) - } - }} - robotName={robot.name} - {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - /> + currentPage === 1 ? ( + { + if (!isCreatingRun) { + resetCreateRun() + setSelectedProtocol(storedProtocol) + } + }} + robotName={robot.name} + {...{ selectedProtocol, runCreationError, runCreationErrorCode }} + /> + ) : ( + pageTwoBody + ) ) : null} - + ) } @@ -225,7 +494,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationErrorCode, robotName, } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'protocol_details', 'shared']) const storedProtocols = useSelector((state: State) => getStoredProtocols(state) ) @@ -401,3 +670,18 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { ) } + +const ENABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + cursor: pointer; +` + +const DISABLED_LINK_CSS = css` + ${TYPOGRAPHY.linkPSemiBold} + color: ${COLORS.grey40}; + cursor: default; + + &:hover { + color: ${COLORS.grey40}; + } +` diff --git a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx index c65e69ce163a..148b9e30e35c 100644 --- a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -53,7 +53,7 @@ export function AvailableRobotOption( registerRobotBusyStatus, } = props const { ip, local, name: robotName } = robot ?? {} - const { t } = useTranslation('protocol_list') + const { t } = useTranslation(['protocol_list', 'branded']) const dispatch = useDispatch() const robotModel = useSelector((state: State) => getRobotModelByName(state, robotName) @@ -160,7 +160,7 @@ export function AvailableRobotOption( > , }} diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 586bc6fe3b92..195001664101 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -3,6 +3,7 @@ import { vi, it, describe, expect, beforeEach } from 'vitest' import { StaticRouter } from 'react-router-dom' import { fireEvent, screen } from '@testing-library/react' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' @@ -22,7 +23,7 @@ import { useFeatureFlag } from '../../../redux/config' import { getNetworkInterfaces } from '../../../redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyService } from '../../../resources/useNotifyService' -import { RunTimeParameter } from '@opentrons/shared-data' +import type { RunTimeParameter } from '@opentrons/shared-data' vi.mock('../../../redux/discovery') vi.mock('../../../redux/robot-update') @@ -48,7 +49,7 @@ const mockRunTimeParameters: RunTimeParameter[] = [ value: false, variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -121,7 +122,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('choose robot slideout title') }) @@ -134,7 +135,7 @@ describe('ChooseRobotSlideout', () => { setSelectedRobot: vi.fn(), title: 'choose robot slideout title', isAnalysisError: true, - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText( 'This protocol failed in-app analysis. It may be unusable on robots without custom software configurations.' @@ -148,7 +149,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') screen.getByText('2 unavailable robots are not listed.') @@ -162,7 +163,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: vi.fn(), title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) screen.getByText('opentrons-robot-name') expect( @@ -177,7 +178,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, })[1] const refreshButton = screen.getByRole('button', { name: 'refresh' }) fireEvent.click(refreshButton) @@ -192,7 +193,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 1 }, }) screen.getByText('Step 1 / 2') @@ -205,7 +206,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, }) screen.getByText('Step 2 / 2') @@ -220,20 +221,51 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, multiSlideout: { currentPage: 2 }, runTimeParametersOverrides: [param], }) screen.getByText(param.displayName) - if (param.type === 'boolean' || 'choices' in param) { + if (param.type === 'bool') { screen.getByText(param.description) - } else { + } + if (param.type === 'int') { screen.getByText(`${param.min}-${param.max}`) } + if (param.type === 'float') { + screen.getByText(`${param.min.toFixed(1)}-${param.max.toFixed(1)}`) + } }) }) + it('renders error message for runtime parameter out of range', () => { + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: OT2_ROBOT_TYPE, + multiSlideout: { currentPage: 2 }, + runTimeParametersOverrides: [ + { + value: 1000, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + ], + }) + screen.getByText('Value must be between 1.5-10.0') + }) + it('defaults to first available robot and allows an available robot to be selected', () => { vi.mocked(getConnectableRobots).mockReturnValue([ { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, @@ -246,7 +278,7 @@ describe('ChooseRobotSlideout', () => { selectedRobot: null, setSelectedRobot: mockSetSelectedRobot, title: 'choose robot slideout title', - robotType: 'OT-2 Standard', + robotType: OT2_ROBOT_TYPE, }) expect(mockSetSelectedRobot).toBeCalledWith({ ...mockConnectableRobot, @@ -264,4 +296,18 @@ describe('ChooseRobotSlideout', () => { ip: 'otherIp', }) }) + + it('sets selected robot to null if no available robots', () => { + vi.mocked(getConnectableRobots).mockReturnValue([]) + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: OT2_ROBOT_TYPE, + }) + expect(mockSetSelectedRobot).toBeCalledWith(null) + }) }) diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index ef5bb8c9368c..fd77056db76a 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -51,7 +51,6 @@ import type { SlideoutProps } from '../../atoms/Slideout' import type { UseCreateRun } from '../../organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import type { State, Dispatch } from '../../redux/types' import type { Robot } from '../../redux/discovery/types' -import { useFeatureFlag } from '../../redux/config' import type { DropdownOption } from '../../atoms/MenuList/DropdownMenu' export const CARD_OUTLINE_BORDER_STYLE = css` @@ -112,7 +111,9 @@ interface ChooseRobotSlideoutProps isAnalysisError?: boolean isAnalysisStale?: boolean showIdleOnly?: boolean - multiSlideout?: { currentPage: number } + multiSlideout?: { currentPage: number } | null + setHasParamError?: (isError: boolean) => void + resetRunTimeParameters?: () => void } export function ChooseRobotSlideout( @@ -135,12 +136,13 @@ export function ChooseRobotSlideout( setSelectedRobot, robotType, showIdleOnly = false, - multiSlideout, + multiSlideout = null, runTimeParametersOverrides, setRunTimeParametersOverrides, + setHasParamError, + resetRunTimeParameters, } = props - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const dispatch = useDispatch() const isScanning = useSelector((state: State) => getScanning(state)) const [targetProps, tooltipProps] = useHoverTooltip() @@ -184,18 +186,27 @@ export function ChooseRobotSlideout( {} ) + const reducerAvailableRobots = healthyReachableRobots.filter(robot => + showIdleOnly ? !robotBusyStatusByName[robot.name] : robot + ) const reducerBusyCount = healthyReachableRobots.filter( robot => robotBusyStatusByName[robot.name] ).length // this useEffect sets the default selection to the first robot in the list. state is managed by the caller React.useEffect(() => { - if (selectedRobot == null && healthyReachableRobots.length > 0) { - setSelectedRobot(healthyReachableRobots[0]) - } else if (healthyReachableRobots.length === 0) { + if ( + (selectedRobot == null || + !reducerAvailableRobots.some( + robot => robot.name === selectedRobot.name + )) && + reducerAvailableRobots.length > 0 + ) { + setSelectedRobot(reducerAvailableRobots[0]) + } else if (reducerAvailableRobots.length === 0) { setSelectedRobot(null) } - }, [healthyReachableRobots, selectedRobot, setSelectedRobot]) + }, [reducerAvailableRobots, selectedRobot, setSelectedRobot]) const unavailableCount = unhealthyReachableRobots.length + unreachableRobots.length @@ -330,6 +341,7 @@ export function ChooseRobotSlideout(
) + const errors: string[] = [] const runTimeParameters = runTimeParametersOverrides?.map((runtimeParam, index) => { if ('choices' in runtimeParam) { @@ -362,25 +374,50 @@ export function ChooseRobotSlideout( } }} title={runtimeParam.displayName} - caption={runtimeParam.description} width="100%" dropdownType="neutral" + tooltipText={runtimeParam.description} /> ) } else if (runtimeParam.type === 'int' || runtimeParam.type === 'float') { const value = runtimeParam.value as number const id = `InputField_${runtimeParam.variableName}_${index.toString()}` + const error = + Number.isNaN(value) || + value < runtimeParam.min || + value > runtimeParam.max + ? t(`value_out_of_range`, { + min: + runtimeParam.type === 'int' + ? runtimeParam.min + : runtimeParam.min.toFixed(1), + max: + runtimeParam.type === 'int' + ? runtimeParam.max + : runtimeParam.max.toFixed(1), + }) + : null + if (error != null) { + errors.push(error) + } return ( { const clone = runTimeParametersOverrides.map((parameter, i) => { if (i === index) { @@ -400,7 +437,7 @@ export function ChooseRobotSlideout( }} /> ) - } else if (runtimeParam.type === 'boolean') { + } else if (runtimeParam.type === 'bool') { return ( 0) + } + const isRestoreDefaultsLinkEnabled = runTimeParametersOverrides?.some( parameter => parameter.value !== parameter.default @@ -468,15 +509,7 @@ export function ChooseRobotSlideout( ? ENABLED_LINK_CSS : DISABLED_LINK_CSS } - onClick={() => { - const clone = runTimeParametersOverrides.map(parameter => ({ - ...parameter, - value: parameter.default, - })) - if (setRunTimeParametersOverrides != null) { - setRunTimeParametersOverrides(clone) - } - }} + onClick={() => resetRunTimeParameters?.()} paddingBottom={SPACING.spacing10} {...targetProps} > @@ -494,7 +527,7 @@ export function ChooseRobotSlideout( ) : null - return multiSlideout != null && enableRunTimeParametersFF ? ( + return multiSlideout != null ? ( { ], {} ) - expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( - 3, + expect(vi.mocked(useCreateRunFromProtocol)).toHaveBeenLastCalledWith( expect.any(Object), { hostname: 'otherIp' }, [], {} ) }) + + it('disables proceed button if no available robots', () => { + vi.mocked(getConnectableRobots).mockReturnValue([]) + render({ + storedProtocolData: storedProtocolDataFixture, + onCloseClick: vi.fn(), + showSlideout: true, + }) + const proceedButton = screen.getByRole('button', { + name: 'Continue to parameters', + }) + expect(proceedButton).toBeDisabled() + }) + + it('renders labware offset data selection and learn more button launches help modal', () => { + render({ + storedProtocolData: storedProtocolDataFixture, + onCloseClick: vi.fn(), + showSlideout: true, + }) + screen.getByText('No offset data available') + const learnMoreLink = screen.getByText('Learn more') + fireEvent.click(learnMoreLink) + screen.getByText( + 'Labware offset data references previous protocol run labware locations to save you time. If all the labware in this protocol have been checked in previous runs, that data will be applied to this run.' + ) + }) }) diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index cc94ee94457a..5dd3278bdfee 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -17,7 +17,6 @@ import { import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' -import { useFeatureFlag } from '../../redux/config' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' @@ -36,7 +35,6 @@ interface ChooseRobotToRunProtocolSlideoutProps extends StyleProps { storedProtocolData: StoredProtocolData onCloseClick: () => void showSlideout: boolean - runTimeParameters?: RunTimeParameter[] } export function ChooseRobotToRunProtocolSlideoutComponent( @@ -54,73 +52,19 @@ export function ChooseRobotToRunProtocolSlideoutComponent( srcFiles, mostRecentAnalysis, } = storedProtocolData - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const [currentPage, setCurrentPage] = React.useState(1) const [selectedRobot, setSelectedRobot] = React.useState(null) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( storedProtocolData, selectedRobot?.name ?? '' ) - - // TODO: (nd: 3/20/24) remove stubs and pull parameters from analysis - const mockRunTimeParameters: RunTimeParameter[] = [ - { - displayName: 'Dry Run', - value: false, - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', - default: false, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - ] - const runTimeParameters: RunTimeParameter[] = mockRunTimeParameters + const runTimeParameters = + storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] const [ runTimeParametersOverrides, setRunTimeParametersOverrides, ] = React.useState(runTimeParameters) + const [hasParamError, setHasParamError] = React.useState(false) const offsetCandidates = useOffsetCandidatesForAnalysis( mostRecentAnalysis, @@ -188,6 +132,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( 'downgrade', ].includes(autoUpdateAction) + const hasRunTimeParameters = runTimeParameters.length > 0 + if ( protocolKey == null || srcFileNames == null || @@ -212,7 +158,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ? mostRecentAnalysis?.robotType ?? null : null - const SinglePageButtonWithoutFF = ( + const singlePageButton = ( ) + const offsetsComponent = ( + + ) + + const resetRunTimeParameters = (): void => { + setRunTimeParametersOverrides( + runTimeParametersOverrides?.map(parameter => ({ + ...parameter, + value: parameter.default, + })) + ) + } + return ( { + onCloseClick() + resetRunTimeParameters() + setCurrentPage(1) + setSelectedRobot(null) + }} title={ - enableRunTimeParametersFF && - runTimeParameters.length > 0 && - currentPage === 2 + hasRunTimeParameters && currentPage === 2 ? t('select_parameters_for_robot', { robot_name: selectedRobot?.name, }) @@ -253,17 +222,10 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setRunTimeParametersOverrides={setRunTimeParametersOverrides} footer={ - {enableRunTimeParametersFF && runTimeParameters.length > 0 ? ( + {hasRunTimeParameters ? ( currentPage === 1 ? ( <> - + {offsetsComponent} setCurrentPage(2)} width="100%" @@ -281,7 +243,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setCurrentPage(1)} width="50%"> {t('shared:change_robot')} - + {isCreatingRun ? ( ) : ( @@ -291,7 +257,10 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) ) : ( - SinglePageButtonWithoutFF + <> + {offsetsComponent} + {singlePageButton} + )}
} @@ -302,7 +271,9 @@ export function ChooseRobotToRunProtocolSlideoutComponent( reset={resetCreateRun} runCreationError={runCreationError} runCreationErrorCode={runCreationErrorCode} - showIdleOnly={true} + showIdleOnly + setHasParamError={setHasParamError} + resetRunTimeParameters={resetRunTimeParameters} /> ) } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts index 0e897881c5c6..c649d2eb8854 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol.ts @@ -14,7 +14,7 @@ import type { HostConfig, LabwareOffsetCreateData, Protocol, - RuntimeParameterCreateData, + RunTimeParameterCreateData, } from '@opentrons/api-client' import type { UseCreateRunMutationOptions } from '@opentrons/react-api-client/src/runs/useCreateRunMutation' import type { CreateProtocolVariables } from '@opentrons/react-api-client/src/protocols/useCreateProtocolMutation' @@ -37,7 +37,7 @@ export function useCreateRunFromProtocol( options: UseCreateRunMutationOptions, hostOverride?: HostConfig | null, labwareOffsets?: LabwareOffsetCreateData[], - runTimeParameterValues?: RuntimeParameterCreateData + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateRun { const contextHost = useHost() const host = @@ -83,7 +83,8 @@ export function useCreateRunFromProtocol( }) }, }, - host + host, + runTimeParameterValues ) let error = @@ -107,7 +108,11 @@ export function useCreateRunFromProtocol( ) => { resetRunMutation() createProtocolRun( - { files: [...srcFiles, ...customLabwareFiles], protocolKey }, + { + files: [...srcFiles, ...customLabwareFiles], + protocolKey, + runTimeParameterValues, + }, ...args ) }, diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 06eae7547599..47c541401491 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -190,11 +190,15 @@ export function CommandText(props: Props): JSX.Element | null { robotType ) : '' - return t('move_to_well', { - well_name: wellName, - labware: getLabwareName(robotSideAnalysis, labwareId), - labware_location: displayLocation, - }) + return ( + + {t('move_to_well', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + })} + + ) } case 'moveLabware': { return ( diff --git a/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx b/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx index d97524f1e594..32ac241955de 100644 --- a/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigFormResetButton.tsx @@ -17,13 +17,13 @@ export interface ButtonProps { export function ConfigFormResetButton(props: ButtonProps): JSX.Element { const { onClick, disabled } = props - const { t } = useTranslation(['shared', 'device_details']) + const { t } = useTranslation(['shared', 'branded']) return ( interface AddFixtureModalProps { cutoutId: CutoutId @@ -52,6 +68,12 @@ interface AddFixtureModalProps { providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } +type OptionStage = + | 'modulesOrFixtures' + | 'fixtureOptions' + | 'moduleOptions' + | 'wasteChuteOptions' + | 'providedOptions' export function AddFixtureModal({ cutoutId, @@ -62,9 +84,26 @@ export function AddFixtureModal({ }: AddFixtureModalProps): JSX.Element { const { t } = useTranslation(['device_details', 'shared']) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const { data: modulesData } = useModulesQuery() const deckConfig = useDeckConfigurationQuery()?.data ?? [] - const [showWasteChuteOptions, setShowWasteChuteOptions] = React.useState( - false + const unconfiguredMods = + modulesData?.data.filter( + attachedMod => + !deckConfig.some( + ({ opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber + ) + ) ?? [] + + let initialStage: OptionStage = SINGLE_CENTER_CUTOUTS.includes(cutoutId) // only mag block (a module) can be configured in column 2 + ? 'moduleOptions' + : 'modulesOrFixtures' + if (providedFixtureOptions != null) { + // only show provided options if given as props + initialStage = 'providedOptions' + } + const [optionStage, setOptionStage] = React.useState( + initialStage ) const modalHeader: ModalHeaderBaseProps = { @@ -72,75 +111,232 @@ export function AddFixtureModal({ slotName: getCutoutDisplayName(cutoutId), }), hasExitIcon: providedFixtureOptions == null, - onClick: () => setShowAddFixtureModal(false), + onClick: () => { + setShowAddFixtureModal(false) + }, } const modalProps: LegacyModalProps = { title: t('add_to_slot', { slotName: getCutoutDisplayName(cutoutId), }), - onClose: () => setShowAddFixtureModal(false), + onClose: () => { + setShowAddFixtureModal(false) + }, closeOnOutsideClick: true, childrenPadding: SPACING.spacing24, width: '26.75rem', } - const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE] - if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { - availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE) + let availableOptions: CutoutConfig[][] = [] + + if (providedFixtureOptions != null) { + availableOptions = providedFixtureOptions?.map(o => [ + { + cutoutId, + cutoutFixtureId: o, + opentronsModuleSerialNumber: undefined, + }, + ]) + } else if (optionStage === 'fixtureOptions') { + if ( + SINGLE_RIGHT_CUTOUTS.includes(cutoutId) || + SINGLE_LEFT_CUTOUTS.includes(cutoutId) + ) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + }, + ], + ] + } + if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + ], + ] + } + } else if (optionStage === 'moduleOptions') { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: MAGNETIC_BLOCK_V1_FIXTURE, + }, + ], + ] + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + availableOptions = [ + ...availableOptions, + [ + { + cutoutId, + cutoutFixtureId: STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + }, + ], + ] + } + if (unconfiguredMods.length > 0) { + if (THERMOCYCLER_MODULE_CUTOUTS.includes(cutoutId)) { + const unconfiguredTCs = unconfiguredMods + .filter(mod => mod.moduleModel === THERMOCYCLER_MODULE_V2) + .map(mod => [ + { + cutoutId: THERMOCYCLER_MODULE_CUTOUTS[0], + cutoutFixtureId: THERMOCYCLER_V2_REAR_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + { + cutoutId: THERMOCYCLER_MODULE_CUTOUTS[1], + cutoutFixtureId: THERMOCYCLER_V2_FRONT_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [...availableOptions, ...unconfiguredTCs] + } + if ( + HEATER_SHAKER_CUTOUTS.includes(cutoutId) && + unconfiguredMods.some(m => m.moduleModel === HEATERSHAKER_MODULE_V1) + ) { + const unconfiguredHeaterShakers = unconfiguredMods + .filter(mod => mod.moduleModel === HEATERSHAKER_MODULE_V1) + .map(mod => [ + { + cutoutId, + cutoutFixtureId: HEATERSHAKER_MODULE_V1_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [...availableOptions, ...unconfiguredHeaterShakers] + } + if ( + TEMPERATURE_MODULE_CUTOUTS.includes(cutoutId) && + unconfiguredMods.some(m => m.moduleModel === TEMPERATURE_MODULE_V2) + ) { + const unconfiguredTemperatureModules = unconfiguredMods + .filter(mod => mod.moduleModel === TEMPERATURE_MODULE_V2) + .map(mod => [ + { + cutoutId, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: mod.serialNumber, + }, + ]) + availableOptions = [ + ...availableOptions, + ...unconfiguredTemperatureModules, + ] + } + } + } else if (optionStage === 'wasteChuteOptions') { + availableOptions = WASTE_CHUTE_FIXTURES.map(fixture => [ + { + cutoutId, + cutoutFixtureId: fixture, + }, + ]) + } + + let nextStageOptions = null + if (optionStage === 'modulesOrFixtures') { + nextStageOptions = ( + <> + {SINGLE_CENTER_CUTOUTS.includes(cutoutId) ? null : ( + { + setOptionStage('fixtureOptions') + }} + isOnDevice={isOnDevice} + /> + )} + { + setOptionStage('moduleOptions') + }} + isOnDevice={isOnDevice} + /> + + ) + } else if ( + optionStage === 'fixtureOptions' && + cutoutId === WASTE_CHUTE_CUTOUT + ) { + nextStageOptions = ( + <> + { + setOptionStage('wasteChuteOptions') + }} + isOnDevice={isOnDevice} + /> + + ) } - const handleAddODD = (requiredFixtureId: CutoutFixtureId): void => { + const handleAddODD = (addedCutoutConfigs: CutoutConfig[]): void => { if (setCurrentDeckConfig != null) setCurrentDeckConfig( (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: CutoutConfig) => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } - : fixture - ) + prevDeckConfig.map((fixture: CutoutConfig) => { + const replacementCutoutConfig = addedCutoutConfigs.find( + c => c.cutoutId === fixture.cutoutId + ) + return replacementCutoutConfig ?? fixture + }) ) setShowAddFixtureModal(false) } - const fixtureOptions = providedFixtureOptions ?? availableFixtures - const fixtureOptionsWithDisplayNames: Array< - [CutoutFixtureId | 'WASTE_CHUTE', string] - > = fixtureOptions.map(fixture => [fixture, getFixtureDisplayName(fixture)]) - - const showSelectWasteChuteOptions = - cutoutId === WASTE_CHUTE_CUTOUT && providedFixtureOptions == null - - const fixtureOptionsWithDisplayNamesAndGenericWasteChute = fixtureOptionsWithDisplayNames.concat( - showSelectWasteChuteOptions - ? [[GENERIC_WASTE_CHUTE_OPTION, t('waste_chute')]] - : [] - ) - - fixtureOptionsWithDisplayNamesAndGenericWasteChute.sort((a, b) => - a[1].localeCompare(b[1]) - ) - - const wasteChuteOptionsWithDisplayNames = WASTE_CHUTE_FIXTURES.map( - fixture => [fixture, getFixtureDisplayName(fixture)] - ).sort((a, b) => a[1].localeCompare(b[1])) as Array<[CutoutFixtureId, string]> - - const displayedFixtureOptions = showWasteChuteOptions - ? wasteChuteOptionsWithDisplayNames - : fixtureOptionsWithDisplayNamesAndGenericWasteChute - - const handleAddDesktop = (requiredFixtureId: CutoutFixtureId): void => { - const newDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } - : fixture - ) + const handleAddDesktop = (addedCutoutConfigs: CutoutConfig[]): void => { + const newDeckConfig = deckConfig.map(fixture => { + const replacementCutoutConfig = addedCutoutConfigs.find( + c => c.cutoutId === fixture.cutoutId + ) + return replacementCutoutConfig ?? fixture + }) updateDeckConfiguration(newDeckConfig) setShowAddFixtureModal(false) } + const fixtureOptions = availableOptions.map(cutoutConfigs => ( + m.serialNumber === cutoutConfigs[0].opentronsModuleSerialNumber + )?.usbPort.port + )} + buttonText={t('add')} + onClickHandler={() => { + isOnDevice + ? handleAddODD(cutoutConfigs) + : handleAddDesktop(cutoutConfigs) + }} + isOnDevice={isOnDevice} + /> + )) + return ( <> {isOnDevice ? ( @@ -155,40 +351,8 @@ export function AddFixtureModal({ {t('add_to_slot_description')} - {displayedFixtureOptions.map( - ([cutoutFixtureOption, fixtureDisplayName]) => { - const onClickHandler = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? () => setShowWasteChuteOptions(true) - : () => handleAddODD(cutoutFixtureOption) - const buttonText = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? t('select_options') - : t('add') - - return ( - - - - {fixtureDisplayName} - - {buttonText} - - - ) - } - )} + {fixtureOptions} + {nextStageOptions} @@ -197,43 +361,15 @@ export function AddFixtureModal({ {t('add_fixture_description')} - {displayedFixtureOptions.map( - ([cutoutFixtureOption, fixtureDisplayName]) => { - const onClickHandler = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? () => setShowWasteChuteOptions(true) - : () => handleAddDesktop(cutoutFixtureOption) - const buttonText = - cutoutFixtureOption === GENERIC_WASTE_CHUTE_OPTION - ? t('select_options') - : t('add') - - return ( - - - - {fixtureDisplayName} - - - {buttonText} - - - - ) - } - )} + {fixtureOptions} + {nextStageOptions} - {showWasteChuteOptions ? ( + {optionStage === 'wasteChuteOptions' ? ( setShowWasteChuteOptions(false)} + onClick={() => { + setOptionStage('fixtureOptions') + }} aria-label="back" paddingX={SPACING.spacing16} marginTop={'1.44rem'} @@ -289,3 +425,41 @@ const GO_BACK_BUTTON_STYLE = css` opacity: 70%; } ` + +interface FixtureOptionProps { + onClickHandler: React.MouseEventHandler + optionName: string + buttonText: string + isOnDevice: boolean +} +export function FixtureOption(props: FixtureOptionProps): JSX.Element { + const { onClickHandler, optionName, buttonText, isOnDevice } = props + return isOnDevice ? ( + + + {props.optionName} + + {props.buttonText} + + ) : ( + + {optionName} + {buttonText} + + ) +} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal.tsx index 12d0f1d39679..9d622a4d4d59 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal.tsx @@ -32,7 +32,7 @@ export function DeckFixtureSetupInstructionsModal({ setShowSetupInstructionsModal, isOnDevice = false, }: DeckFixtureSetupInstructionsModalProps): JSX.Element { - const { i18n, t } = useTranslation(['device_details', 'shared']) + const { i18n, t } = useTranslation(['device_details', 'shared', 'branded']) const modalHeader: ModalHeaderBaseProps = { title: t('deck_fixture_setup_instructions'), iconName: 'information', @@ -62,7 +62,7 @@ export function DeckFixtureSetupInstructionsModal({ {t('deck_fixture_setup_modal_top_description')} - {t('deck_fixture_setup_modal_bottom_description')} + {t('branded:deck_fixture_setup_modal_bottom_description')} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index 846c060dc27e..74d150d92dc8 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -4,6 +4,7 @@ import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { @@ -17,6 +18,7 @@ import { AddFixtureModal } from '../AddFixtureModal' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' +import type { Modules } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') const mockSetShowAddFixtureModal = vi.fn() @@ -45,23 +47,25 @@ describe('Touchscreen AddFixtureModal', () => { vi.mocked(useDeckConfigurationQuery).mockReturnValue(({ data: [], } as unknown) as UseQueryResult) + vi.mocked(useModulesQuery).mockReturnValue(({ + data: { data: [] }, + } as unknown) as UseQueryResult) }) it('should render text and buttons', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' + 'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.' ) - screen.getByText('Staging area slot') - screen.getByText('Trash bin') - screen.getByText('Waste chute') - expect(screen.getAllByText('Add').length).toBe(2) - expect(screen.getAllByText('Select options').length).toBe(1) + screen.getByText('Fixtures') + screen.getByText('Modules') + expect(screen.getAllByText('Select options').length).toBe(2) }) - it('should a mock function when tapping app button', () => { + it('should set deck config when tapping add button', () => { render(props) + fireEvent.click(screen.getAllByText('Select options')[1]) fireEvent.click(screen.getAllByText('Add')[0]) expect(mockSetCurrentDeckConfig).toHaveBeenCalled() }) @@ -74,7 +78,7 @@ describe('Touchscreen AddFixtureModal', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' + 'Choose an item below to add to your deck configuration. It will be referenced during protocol analysis.' ) expect(screen.queryByText('Staging area slot')).toBeNull() screen.getByText('Trash bin') @@ -105,8 +109,12 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot D3') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Staging area slot') screen.getByText('Trash bin') screen.getByText('Waste chute') @@ -121,8 +129,11 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot A1') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Trash bin') screen.getByRole('button', { name: 'Add' }) }) @@ -132,23 +143,39 @@ describe('Desktop AddFixtureModal', () => { render(props) screen.getByText('Add to slot B3') screen.getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' ) + screen.getByText('Fixtures') + screen.getByText('Modules') + fireEvent.click(screen.getAllByText('Select options')[0]) screen.getByText('Staging area slot') screen.getByText('Trash bin') expect(screen.getAllByRole('button', { name: 'Add' }).length).toBe(2) }) - it('should call a mock function when clicking add button', () => { + it('should only render module options in column 2', () => { + props = { ...props, cutoutId: 'cutoutB2' } + render(props) + screen.getByText('Add to slot B2') + screen.getByText( + 'Add this item to your deck configuration. It will be referenced during protocol analysis.' + ) + screen.getByText('Magnetic Block GEN1') + expect(screen.getByRole('button', { name: 'Add' })).toBeInTheDocument() + }) + + it('should call update deck config when add button is clicked', () => { props = { ...props, cutoutId: 'cutoutA1' } render(props) - fireEvent.click(screen.getByRole('button', { name: 'Add' })) + fireEvent.click(screen.getAllByText('Select options')[1]) + fireEvent.click(screen.getByText('Add')) expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) it('should display appropriate Waste Chute options when the generic Waste Chute button is clicked', () => { render(props) - fireEvent.click(screen.getByRole('button', { name: 'Select options' })) + fireEvent.click(screen.getAllByText('Select options')[0]) // click fixtures + fireEvent.click(screen.getByRole('button', { name: 'Select options' })) // click waste chute options expect(screen.getAllByRole('button', { name: 'Add' }).length).toBe( WASTE_CHUTE_FIXTURES.length ) @@ -161,6 +188,7 @@ describe('Desktop AddFixtureModal', () => { it('should allow a user to exit the Waste Chute submenu by clicking "go back"', () => { render(props) + fireEvent.click(screen.getAllByText('Select options')[0]) // click fixtures fireEvent.click(screen.getByRole('button', { name: 'Select options' })) fireEvent.click(screen.getByText('Go back')) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index f3b008320af9..5c8d3974dc87 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -6,6 +6,7 @@ import { describe, it, beforeEach, vi, afterEach } from 'vitest' import { DeckConfigurator } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' @@ -60,6 +61,7 @@ describe('DeviceDetailsDeckConfiguration', () => { props = { robotName: ROBOT_NAME, } + vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [] } as any) vi.mocked(useUpdateDeckConfigurationMutation).mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, @@ -89,7 +91,7 @@ describe('DeviceDetailsDeckConfiguration', () => { screen.getByText('otie deck configuration') screen.getByRole('button', { name: 'Setup Instructions' }) screen.getByText('Location') - screen.getByText('Fixture') + screen.getByText('Deck hardware') screen.getByText('mock DeckConfigurator') }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 9c1d852253ac..0103fe25051c 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -21,6 +21,7 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, + useModulesQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { @@ -30,6 +31,10 @@ import { SINGLE_SLOT_FIXTURES, SINGLE_LEFT_SLOT_FIXTURE, SINGLE_RIGHT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_LEFT_CUTOUTS, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' @@ -39,7 +44,7 @@ import { AddFixtureModal } from './AddFixtureModal' import { useIsRobotViewable, useRunStatuses } from '../Devices/hooks' import { useIsEstopNotDisengaged } from '../../resources/devices/hooks/useIsEstopNotDisengaged' -import type { CutoutId } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' const DECK_CONFIG_REFETCH_INTERVAL = 5000 const RUN_REFETCH_INTERVAL = 5000 @@ -48,10 +53,14 @@ interface DeviceDetailsDeckConfigurationProps { robotName: string } +function getDisplayLocationForCutoutIds(cutouts: CutoutId[]): string { + return cutouts.map(cutoutId => getCutoutDisplayName(cutoutId)).join(' + ') +} + export function DeviceDetailsDeckConfiguration({ robotName, }: DeviceDetailsDeckConfigurationProps): JSX.Element | null { - const { t } = useTranslation('device_details') + const { t, i18n } = useTranslation('device_details') const [ showSetupInstructionsModal, setShowSetupInstructionsModal, @@ -63,9 +72,11 @@ export function DeviceDetailsDeckConfiguration({ null ) + const { data: modulesData } = useModulesQuery() const deckConfig = useDeckConfigurationQuery({ refetchInterval: DECK_CONFIG_REFETCH_INTERVAL }) .data ?? [] + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const { isRunRunning } = useRunStatuses() const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ @@ -80,26 +91,109 @@ export function DeviceDetailsDeckConfiguration({ setShowAddFixtureModal(true) } - const handleClickRemove = (cutoutId: CutoutId): void => { - const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - const singleSlotFixture = isRightCutout - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE + const handleClickRemove = ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ): void => { + let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE + } - const newDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: singleSlotFixture } - : fixture - ) + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + let newDeckConfig = deckConfig + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId in groupMap + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } else { + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId === cutoutId + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } updateDeckConfiguration(newDeckConfig) } // do not show standard slot in fixture display list - const fixtureDisplayList = deckConfig.filter( - fixture => - fixture.cutoutFixtureId != null && - !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + const { displayList: fixtureDisplayList } = deckConfig.reduce<{ + displayList: Array<{ displayLocation: string; displayName: string }> + groupedCutoutIds: CutoutId[] + }>( + (acc, { cutoutId, cutoutFixtureId, opentronsModuleSerialNumber }) => { + if ( + cutoutFixtureId == null || + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) { + return acc + } + const displayName = getFixtureDisplayName( + cutoutFixtureId, + modulesData?.data.find( + m => m.serialNumber === opentronsModuleSerialNumber + )?.usbPort.port + ) + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + const groupedCutoutIds = Object.keys(groupMap) as CutoutId[] + const displayLocation = getDisplayLocationForCutoutIds(groupedCutoutIds) + if (acc.groupedCutoutIds.includes(cutoutId)) { + return acc // only list grouped fixtures once + } else { + return { + displayList: [...acc.displayList, { displayLocation, displayName }], + groupedCutoutIds: [...acc.groupedCutoutIds, ...groupedCutoutIds], + } + } + } + return { + ...acc, + displayList: [ + ...acc.displayList, + { + displayLocation: getDisplayLocationForCutoutIds([cutoutId]), + displayName, + }, + ], + } + }, + { displayList: [], groupedCutoutIds: [] } ) return ( @@ -132,11 +226,7 @@ export function DeviceDetailsDeckConfiguration({ width="100%" borderBottom={BORDERS.lineBorder} > - + {`${robotName} ${t('deck_configuration')}`} cutoutId) } deckConfig={deckConfig} handleClickAdd={handleClickAdd} @@ -197,30 +287,28 @@ export function DeviceDetailsDeckConfiguration({ width="32rem" > - {t('location')} - {t('fixture')} + {t('location')} + + {i18n.format(t('deck_hardware'), 'capitalize')} + {fixtureDisplayList.length > 0 ? ( - fixtureDisplayList.map(fixture => ( + fixtureDisplayList.map(({ displayLocation, displayName }) => ( - - {getCutoutDisplayName(fixture.cutoutId)} - - - {getFixtureDisplayName(fixture.cutoutFixtureId)} - + {displayLocation} + {displayName} )) ) : ( @@ -248,11 +336,7 @@ export function DeviceDetailsDeckConfiguration({ paddingBottom={SPACING.spacing24} width="100%" > - + {t('offline_deck_configuration')} diff --git a/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx b/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx index 560eedb235b8..03cbde6898a4 100644 --- a/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx +++ b/app/src/organisms/Devices/ConnectionTroubleshootingModal.tsx @@ -48,7 +48,7 @@ export function ConnectionTroubleshootingModal(props: Props): JSX.Element { steps={[t('restart_the_robot'), t('restart_the_app')]} /> - {t('contact_support_for_connection_help', { + {t('branded:contact_support_for_connection_help', { support_email: SUPPORT_EMAIL, })} diff --git a/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx index 7dca3abc6401..698a9fc2e861 100644 --- a/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx +++ b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx @@ -183,7 +183,9 @@ export function FlexPipetteCard({ subsystemUpdateData == null ? ( { - const { t } = useTranslation('protocol_details') - const { attachPipetteRequired, calibratePipetteRequired, @@ -97,18 +94,15 @@ export const ProtocolRunModuleControls = ({ const rightColumnModules = attachedModules?.slice(halfAttachedModulesSize) return attachedModules.length === 0 ? ( - - - {t('connect_modules_to_see_controls')} - - - ) : ( + + + ) : ( + 0 - // ToDo (kk:03/19/2024) this will be replaced with the boolean from values check result - const dummyBoolean = true + const hasCustomValues = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) - // ToDO (kk:03/18/2024) Need to add Chip to updated runTime parameter value - // This part will be implemented in a following PR since need to runTime parameter slideout return ( <> {hasParameter ? ( - {dummyBoolean ? t('custom_values') : t('default_values')} + {hasCustomValues ? t('custom_values') : t('default_values')} ) : null} @@ -221,55 +79,28 @@ export function ProtocolRunRuntimeParameters({ {!hasParameter ? ( - + ) : ( <> - + {t('name')} {t('value')} - + {runTimeParameters.map( - (parameter: RunTimeParameter, index: number) => { - return ( - - - - {parameter.displayName} - - - - - - {formatRunTimeParameterValue(parameter, t)} - - {/* ToDo (kk:03/19/2024) need to implement a logic when be is ready */} - {index % 2 === 0 ? ( - - ) : null} - - - - ) - } + (parameter: RunTimeParameter, index: number) => ( + + ) )} @@ -280,16 +111,84 @@ export function ProtocolRunRuntimeParameters({ ) } +interface StyledTableRowComponentProps { + parameter: RunTimeParameter + index: number + isLast: boolean + t: any +} + +const StyledTableRowComponent = ( + props: StyledTableRowComponentProps +): JSX.Element => { + const { parameter, index, isLast, t } = props + const [targetProps, tooltipProps] = useHoverTooltip() + return ( + + + + {parameter.displayName} + + {parameter.description != null ? ( + <> + + + + + {parameter.description} + + + ) : null} + + + + + {formatRunTimeParameterValue(parameter, t)} + + {parameter.value !== parameter.default ? ( + + ) : null} + + + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; text-align: left; ` +const StyledTableHeaderContainer = styled.thead` + display: grid; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; + border-bottom: ${BORDERS.lineBorder}; +` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; - border-bottom: ${BORDERS.lineBorder}; + padding-bottom: ${SPACING.spacing8}; ` interface StyledTableRowProps { @@ -297,16 +196,21 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + display: grid; + grid-template-columns: 0.35fr 0.35fr; + grid-gap: ${SPACING.spacing48}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; ` interface StyledTableCellProps { - isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` - padding-left: ${SPACING.spacing8}; - padding-top: ${SPACING.spacing12}; - padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + align-items: ${ALIGN_CENTER}; + display: ${props => (props.display != null ? props.display : 'table-cell')}; + padding: ${SPACING.spacing8} 0; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` diff --git a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx b/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx index dbaeff488b81..e03287f59594 100644 --- a/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/RunFailedModal.tsx @@ -51,7 +51,7 @@ export function RunFailedModal({ setShowRunFailedModal, highestPriorityError, }: RunFailedModalProps): JSX.Element | null { - const { i18n, t } = useTranslation(['run_details', 'shared']) + const { i18n, t } = useTranslation(['run_details', 'shared', 'branded']) const modalProps: LegacyModalProps = { type: 'error', title: t('run_failed_modal_title'), @@ -89,7 +89,7 @@ export function RunFailedModal({
- {t('run_failed_modal_description_desktop')} + {t('branded:run_failed_modal_description_desktop')} { - const { t } = useTranslation(['protocol_setup', 'shared']) + const { t } = useTranslation(['protocol_setup', 'shared', 'branded']) const moduleName = getModuleName(props.type) return createPortal( - {t(`secure_labware_explanation_${snakeCase(moduleName)}`)} + {t(`branded:secure_labware_explanation_${snakeCase(moduleName)}`)} { 'Opentrons recommends ensuring your labware locks to the Magnetic Module by adjusting the black plate bracket on top of the module.' ) screen.getByText( - 'Please note there are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the modules thumb screw (the silver knob on the front).' + "There are two sizes of plate brackets supplied with your module: standard and deep well. These brackets can be removed and swapped by unscrewing the module's thumb screw (the silver knob on the front)." ) }) it('should render magnetic module type modal and call onCloseClick when button is pressed', () => { @@ -43,7 +43,7 @@ describe('SecureLabwareModal', () => { render(props) screen.getByText('Securing labware to the Thermocycler') screen.getByText( - 'Opentrons recommends securing your labware to the Thermocycler module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.' + 'Opentrons recommends securing your labware to the Thermocycler Module by closing its latch. Doing so ensures level and accurate plate placement for optimal results.' ) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/HowLPCWorksModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/HowLPCWorksModal.tsx index 52f5d00c758a..4e519d8af724 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/HowLPCWorksModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/HowLPCWorksModal.tsx @@ -22,7 +22,7 @@ interface HowLPCWorksModalProps { } export const HowLPCWorksModal = (props: HowLPCWorksModalProps): JSX.Element => { - const { t } = useTranslation(['protocol_setup', 'shared']) + const { t } = useTranslation(['protocol_setup', 'shared', 'branded']) return createPortal( { /> - {t('why_use_lpc')} + {t('branded:why_use_lpc')} void + onCloseClick: () => void + deckDef: DeckDefinition + isOnDevice: boolean + requiredModuleModel: ModuleModel +} + +export const ChooseModuleToConfigureModal = ( + props: ChooseModuleToConfigureModalProps +): JSX.Element => { + const { + handleConfigureModule, + onCloseClick, + deckDef, + requiredModuleModel, + isOnDevice, + } = props + const { t } = useTranslation(['protocol_setup', 'shared']) + const attachedModules = useModulesQuery().data?.data ?? [] + const deckConfig = useDeckConfigurationQuery()?.data ?? [] + const unconfiguredModuleMatches = + attachedModules.filter( + attachedMod => + attachedMod.moduleModel === requiredModuleModel && + !deckConfig.some( + ({ opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber + ) + ) ?? [] + + const connectedOptions: ModuleFixtureOption[] = unconfiguredModuleMatches.map( + attachedMod => ({ + moduleModel: attachedMod.moduleModel, + usbPort: attachedMod.usbPort.port, + serialNumber: attachedMod.serialNumber, + }) + ) + const passiveOptions: ModuleFixtureOption[] = + requiredModuleModel === MAGNETIC_BLOCK_V1 + ? [{ moduleModel: MAGNETIC_BLOCK_V1 }] + : [] + const fixtureOptions = [...connectedOptions, ...passiveOptions].map( + ({ moduleModel, serialNumber, usbPort }) => { + const moduleFixtures = getCutoutFixturesForModuleModel( + moduleModel, + deckDef + ) + return ( + { + handleConfigureModule(serialNumber) + }} + optionName={getFixtureDisplayName(moduleFixtures[0].id, usbPort)} + buttonText={t('shared:add')} + isOnDevice={isOnDevice} + /> + ) + } + ) + + return createPortal( + isOnDevice ? ( + + + + + {fixtureOptions} + + + + + ) : ( + + + + {t('deck_conflict')} + + + } + onClose={onCloseClick} + width="27.75rem" + > + + + + {fixtureOptions} + + + + + ), + getTopPortalEl() + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index b4a8e634b0dd..c696b4ecbdf1 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -25,11 +25,10 @@ import { getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, - SINGLE_RIGHT_CUTOUTS, - SINGLE_LEFT_SLOT_FIXTURE, - SINGLE_RIGHT_SLOT_FIXTURE, THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, + getCutoutFixturesForModuleModel, + getFixtureIdByCutoutIdFromModuleSlotName, } from '@opentrons/shared-data' import { getTopPortalEl } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' @@ -41,11 +40,14 @@ import type { CutoutId, CutoutFixtureId, ModuleModel, + DeckDefinition, } from '@opentrons/shared-data' +import { ChooseModuleToConfigureModal } from './ChooseModuleToConfigureModal' interface LocationConflictModalProps { onCloseClick: () => void cutoutId: CutoutId + deckDef: DeckDefinition missingLabwareDisplayName?: string | null requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel @@ -61,9 +63,12 @@ export const LocationConflictModal = ( missingLabwareDisplayName, requiredFixtureId, requiredModule, + deckDef, isOnDevice = false, } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) + + const [showModuleSelect, setShowModuleSelect] = React.useState(false) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const deckConfigurationAtLocationFixtureId = deckConfig.find( @@ -89,39 +94,54 @@ export const LocationConflictModal = ( ? getFixtureDisplayName(deckConfigurationAtA1) : currentFixtureDisplayName + const handleConfigureModule = (moduleSerialNumber?: string): void => { + if (requiredModule != null) { + const slotName = cutoutId.replace('cutout', '') + const moduleFixtures = getCutoutFixturesForModuleModel( + requiredModule, + deckDef + ) + const moduleFixtureIdByCutoutId = getFixtureIdByCutoutIdFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + + const newDeckConfig = deckConfig.map(existingCutoutConfig => { + const replacementCutoutFixtureId = + moduleFixtureIdByCutoutId[existingCutoutConfig.cutoutId] + return existingCutoutConfig.cutoutId in moduleFixtureIdByCutoutId && + replacementCutoutFixtureId != null + ? { + ...existingCutoutConfig, + cutoutFixtureId: replacementCutoutFixtureId, + opentronsModuleSerialNumber: moduleSerialNumber, + } + : existingCutoutConfig + }) + updateDeckConfiguration(newDeckConfig) + } + onCloseClick() + } + const handleUpdateDeck = (): void => { - if (requiredFixtureId != null) { + if (requiredModule != null) { + setShowModuleSelect(true) + } else if (requiredFixtureId != null) { const newRequiredFixtureDeckConfig = deckConfig.map(fixture => fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: requiredFixtureId } + ? { + ...fixture, + cutoutFixtureId: requiredFixtureId, + opentronsModuleSerialNumber: undefined, + } : fixture ) - updateDeckConfiguration(newRequiredFixtureDeckConfig) + onCloseClick() } else { - const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - const singleSlotFixture = isRightCutout - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE - - const newSingleSlotDeckConfig = deckConfig.map(fixture => - fixture.cutoutId === cutoutId - ? { ...fixture, cutoutFixtureId: singleSlotFixture } - : fixture - ) - - // add A1 and B1 single slot config for thermocycler - const newThermocyclerDeckConfig = isThermocycler - ? newSingleSlotDeckConfig.map(fixture => - fixture.cutoutId === 'cutoutA1' || fixture.cutoutId === 'cutoutB1' - ? { ...fixture, cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE } - : fixture - ) - : newSingleSlotDeckConfig - - updateDeckConfiguration(newThermocyclerDeckConfig) + onCloseClick() } - onCloseClick() } let protocolSpecifiesDisplayName = '' @@ -133,6 +153,18 @@ export const LocationConflictModal = ( protocolSpecifiesDisplayName = getModuleDisplayName(requiredModule) } + if (showModuleSelect && requiredModule) { + return createPortal( + , + getTopPortalEl() + ) + } return createPortal( isOnDevice ? ( unknown -} - -export const MultipleModulesModal = ( - props: MultipleModulesModalProps -): JSX.Element => { - const { t } = useTranslation(['protocol_setup', 'shared']) - const isOnDevice = useSelector(getIsOnDevice) - return createPortal( - isOnDevice ? ( - - - {t('multiple_of_most_modules')} - 2 temperature modules plugged into the usb ports - - - ) : ( - - - - - - {t('multiple_modules_explanation')} - - - {t('multiple_modules_learn_more')} - - - - {t('example')} - - - {t('multiple_modules_example')} - - 2 temperature modules plugged into the usb ports - - - {t('shared:close')} - - - - ), - getTopPortalEl() - ) -} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx new file mode 100644 index 000000000000..eaac0c079a68 --- /dev/null +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/OT2MultipleModulesHelp.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { + ALIGN_FLEX_END, + Box, + DIRECTION_COLUMN, + DIRECTION_ROW, + Flex, + Icon, + Link, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { getTopPortalEl } from '../../../../App/portal' +import { Banner } from '../../../../atoms/Banner' +import { LegacyModal } from '../../../../molecules/LegacyModal' +import multipleModuleHelp from '../../../../assets/images/Moam_modal_image.png' + +const HOW_TO_MULTIPLE_MODULES_HREF = + 'https://support.opentrons.com/s/article/Using-modules-of-the-same-type-on-the-OT-2' + +export function OT2MultipleModulesHelp(): JSX.Element { + const { t } = useTranslation(['protocol_setup', 'shared']) + const [ + showMultipleModulesModal, + setShowMultipleModulesModal, + ] = React.useState(false) + + const onCloseClick = (): void => { + setShowMultipleModulesModal(false) + } + return ( + <> + + setShowMultipleModulesModal(true)} + closeButton={ + + {t('learn_more')} + + } + > + + + {t('multiple_modules')} + + {t('view_moam')} + + + + {showMultipleModulesModal + ? createPortal( + + + + + + {t('multiple_modules_explanation')} + + + {t('multiple_modules_learn_more')} + + + + {t('example')} + + + + {t('multiple_modules_example')} + + + 2 temperature modules plugged into the usb ports + + + {t('shared:close')} + + + , + getTopPortalEl() + ) + : null} + + ) +} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 4e65fd3759da..b8ad582af17a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -16,8 +16,11 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_MODULE_ADDRESSABLE_AREAS, + FLEX_ROBOT_TYPE, SINGLE_SLOT_FIXTURES, getCutoutDisplayName, + getDeckDefFromRobotType, getFixtureDisplayName, } from '@opentrons/shared-data' import { StatusLabel } from '../../../../atoms/StatusLabel' @@ -27,73 +30,47 @@ import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' import { DeckFixtureSetupInstructionsModal } from '../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +import type { DeckDefinition } from '@opentrons/shared-data' import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' interface SetupFixtureListProps { deckConfigCompatibility: CutoutConfigAndCompatibility[] } - +/** + * List items of all "non-module" fixtures e.g. staging slot, waste chute, trash bin... + * @param props + * @returns JSX.Element + */ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { const { deckConfigCompatibility } = props - const { t, i18n } = useTranslation('protocol_setup') + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) return ( <> - - - {i18n.format(t('fixture_name'), 'capitalize')} - - - {t('location')} - - - {t('status')} - - - - {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { - return ( - - ) - })} - + {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { + return cutoutConfigAndCompatibility.requiredAddressableAreas.some(raa => + FLEX_MODULE_ADDRESSABLE_AREAS.includes(raa) + ) ? null : ( // don't list modules here, they're covered by SetupModuleList + + ) + })} ) } -interface FixtureListItemProps extends CutoutConfigAndCompatibility {} +interface FixtureListItemProps extends CutoutConfigAndCompatibility { + deckDef: DeckDefinition +} export function FixtureListItem({ cutoutId, cutoutFixtureId, compatibleCutoutFixtureIds, missingLabwareDisplayName, + deckDef, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') @@ -155,6 +132,7 @@ export function FixtureListItem({ setShowLocationConflictModal(false)} cutoutId={cutoutId} + deckDef={deckDef} missingLabwareDisplayName={missingLabwareDisplayName} requiredFixtureId={compatibleCutoutFixtureIds[0]} /> diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 8f0879e96094..cf258c2bc00a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -27,11 +27,11 @@ import { HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1, + OT2_ROBOT_TYPE, TC_MODULE_LOCATION_OT2, TC_MODULE_LOCATION_OT3, } from '@opentrons/shared-data' -import { Banner } from '../../../../atoms/Banner' import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { Tooltip } from '../../../../atoms/Tooltip' @@ -48,7 +48,7 @@ import { useRunCalibrationStatus, } from '../../hooks' import { LocationConflictModal } from './LocationConflictModal' -import { MultipleModulesModal } from './MultipleModulesModal' +import { OT2MultipleModulesHelp } from './OT2MultipleModulesHelp' import { UnMatchedModuleWarning } from './UnMatchedModuleWarning' import { getModuleImage } from './utils' @@ -70,7 +70,6 @@ interface SetupModulesListProps { export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const { robotName, runId } = props - const { t } = useTranslation('protocol_setup') const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( runId ) @@ -85,125 +84,53 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const calibrationStatus = useRunCalibrationStatus(robotName, runId) - const [ - showMultipleModulesModal, - setShowMultipleModulesModal, - ] = React.useState(false) - const moduleModels = map( moduleRenderInfoForProtocolById, ({ moduleDef }) => moduleDef.model ) - - const hasADuplicateModule = new Set(moduleModels).size !== moduleModels.length - + const showOT2MoamHelp = + robotModel === OT2_ROBOT_TYPE && + new Set(moduleModels).size !== moduleModels.length return ( <> - {showMultipleModulesModal ? ( - setShowMultipleModulesModal(false)} - /> - ) : null} - {hasADuplicateModule ? ( - - setShowMultipleModulesModal(true)} - closeButton={ - - {t('learn_more')} - - } - > - - - {t('multiple_modules')} - - {t('view_moam')} - - - - ) : null} + {showOT2MoamHelp ? : null} {remainingAttachedModules.length !== 0 && missingModuleIds.length !== 0 ? ( ) : null} - - - {t('module_name')} - - - {t('location')} - - - {t('status')} - - - - {map( - moduleRenderInfoForProtocolById, - ({ - moduleDef, - attachedModuleMatch, - slotName, - moduleId, - conflictedFixture, - }) => { - return ( - - ) - } - )} - + + {map( + moduleRenderInfoForProtocolById, + ({ + moduleDef, + attachedModuleMatch, + slotName, + moduleId, + conflictedFixture, + }) => { + return ( + + ) + } + )} ) } @@ -358,13 +285,13 @@ export function ModulesListItem({ onCloseClick={() => setShowLocationConflictModal(false)} cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} + deckDef={deckDef} /> ) : null} {showModuleWizard && attachedModuleMatch != null ? ( setShowModuleWizard(false)} - initialSlotName={slotName} isPrepCommandLoading={isCommandMutationLoading} prepCommandErrorMessage={ prepCommandErrorMessage === '' ? undefined : prepCommandErrorMessage @@ -404,13 +331,26 @@ export function ModulesListItem({ {subText} - - {getModuleType(moduleModel) === 'thermocyclerModuleType' - ? isFlex - ? TC_MODULE_LOCATION_OT3 - : TC_MODULE_LOCATION_OT2 - : slotName} - + + + {getModuleType(moduleModel) === 'thermocyclerModuleType' + ? isFlex + ? TC_MODULE_LOCATION_OT3 + : TC_MODULE_LOCATION_OT2 + : slotName} + + {attachedModuleMatch?.usbPort.port != null ? ( + + {t('usb_port_number', { + port: attachedModuleMatch.usbPort.port, + })} + + ) : null} + { onCloseClick: vi.fn(), cutoutId: 'cutoutB3', requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, + deckDef: ot3StandardDeckV5 as any, } + vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [mockFixture], } as UseQueryResult) @@ -64,18 +69,23 @@ describe('LocationConflictModal', () => { expect(mockUpdate).toHaveBeenCalled() }) it('should render the modal information for a module fixture conflict', () => { + vi.mocked(useModulesQuery).mockReturnValue({ + data: { data: [mockHeaterShaker] }, + } as any) props = { onCloseClick: vi.fn(), cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', + deckDef: ot3StandardDeckV5 as any, } render(props) screen.getByText('Protocol specifies') screen.getByText('Currently configured') - screen.getByText('Heater-Shaker Module GEN1') fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) expect(props.onCloseClick).toHaveBeenCalled() fireEvent.click(screen.getByRole('button', { name: 'Update deck' })) + screen.getByText('Heater-Shaker Module GEN1 in USB-1') + fireEvent.click(screen.getByRole('button', { name: 'add' })) expect(mockUpdate).toHaveBeenCalled() }) it('should render the modal information for a single slot fixture conflict', () => { @@ -92,6 +102,7 @@ describe('LocationConflictModal', () => { cutoutId: 'cutoutB1', requiredFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, missingLabwareDisplayName: 'a tiprack', + deckDef: ot3StandardDeckV5 as any, } render(props) screen.getByText('Deck location conflict') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx similarity index 60% rename from app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx rename to app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx index 532ab57c39bb..984dc1e57e5c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/MultipleModuleModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/OT2MultipleModulesHelp.test.tsx @@ -5,31 +5,30 @@ import { describe, it, beforeEach, vi, expect } from 'vitest' import { renderWithProviders } from '../../../../../__testing-utils__' import { i18n } from '../../../../../i18n' import { getIsOnDevice } from '../../../../../redux/config' -import { MultipleModulesModal } from '../MultipleModulesModal' +import { OT2MultipleModulesHelp } from '../OT2MultipleModulesHelp' vi.mock('../../../../../redux/config') -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { +const render = () => + renderWithProviders(, { i18nInstance: i18n, })[0] -} -describe('MultipleModulesModal', () => { - let props: React.ComponentProps +describe('OT2MultipleModulesHelp', () => { beforeEach(() => { - props = { onCloseClick: vi.fn() } vi.mocked(getIsOnDevice).mockReturnValue(false) }) it('should render the correct header', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) screen.getByRole('heading', { name: 'Setting up multiple modules of the same type', }) }) it('should render the correct body', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) screen.getByText( 'To use more than one of the same module in a protocol, you first need to plug in the module that’s called first in your protocol to the lowest numbered USB port on the robot. Continue in the same manner with additional modules.' ) @@ -40,7 +39,8 @@ describe('MultipleModulesModal', () => { screen.getByAltText('2 temperature modules plugged into the usb ports') }) it('should render a link to the learn more page', () => { - render(props) + render() + fireEvent.click(screen.getByText('Learn more')) expect( screen .getByRole('link', { @@ -51,23 +51,13 @@ describe('MultipleModulesModal', () => { 'https://support.opentrons.com/s/article/Using-modules-of-the-same-type-on-the-OT-2' ) }) - it('should call onCloseClick when the close button is pressed', () => { - render(props) - expect(props.onCloseClick).not.toHaveBeenCalled() + it('should call close info modal when the close button is pressed', () => { + render() + fireEvent.click(screen.getByText('Learn more')) const closeButton = screen.getByRole('button', { name: 'close' }) fireEvent.click(closeButton) - expect(props.onCloseClick).toHaveBeenCalled() - }) - it('should render the correct text and img for on device display', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - render(props) - screen.getByText( - 'You can use multiples of most module types within a single Python protocol by connecting and loading the modules in a specific order. The robot will initialize the matching module attached to the lowest numbered port first, regardless of what deck slot it occupies.' - ) - const img = screen.getByRole('img') - expect(img.getAttribute('src')).toBe( - '/app/src/assets/images/on-device-display/multiple_modules_modal.png' - ) - screen.getByAltText('2 temperature modules plugged into the usb ports') + expect( + screen.queryByText('Setting up multiple modules of the same type') + ).toBeNull() }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index 69813bdbd8ff..2aba1928899a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -81,11 +81,8 @@ describe('SetupFixtureList', () => { ) }) - it('should render the headers and a fixture with configured status', () => { + it('should a fixture with configured status', () => { render(props) - screen.getByText('Fixture') - screen.getByText('Location') - screen.getByText('Status') screen.getByText('Waste chute with staging area slot') screen.getByRole('button', { name: 'View setup instructions' }) screen.getByText('D3') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 05df2fc9ceff..b784e25d0c92 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -3,7 +3,11 @@ import { when } from 'vitest-when' import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, beforeEach, expect, vi } from 'vitest' import { renderWithProviders } from '../../../../../__testing-utils__' -import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { mockMagneticModule as mockMagneticModuleFixture, @@ -20,16 +24,17 @@ import { ModuleWizardFlows } from '../../../../ModuleWizardFlows' import { useIsFlex, useModuleRenderInfoForProtocolById, - useRunHasStarted, useUnmatchedModulesForProtocol, useRunCalibrationStatus, + useRobot, } from '../../../hooks' -import { MultipleModulesModal } from '../MultipleModulesModal' +import { OT2MultipleModulesHelp } from '../OT2MultipleModulesHelp' import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '../LocationConflictModal' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { DiscoveredRobot } from '../../../../../redux/discovery/types' vi.mock('@opentrons/react-api-client') vi.mock('../../../hooks') @@ -37,7 +42,7 @@ vi.mock('../LocationConflictModal') vi.mock('../UnMatchedModuleWarning') vi.mock('../../../../ModuleCard/ModuleSetupModal') vi.mock('../../../../ModuleWizardFlows') -vi.mock('../MultipleModulesModal') +vi.mock('../OT2MultipleModulesHelp') vi.mock('../../../../../resources/runs') vi.mock('../../../../../redux/config') @@ -92,6 +97,9 @@ describe('SetupModulesList', () => { robotName: ROBOT_NAME, runId: RUN_ID, } + when(vi.mocked(useRobot)) + .calledWith(ROBOT_NAME) + .thenReturn({ robotModel: FLEX_ROBOT_TYPE } as DiscoveredRobot) mockChainLiveCommands = vi.fn() mockChainLiveCommands.mockResolvedValue(null) vi.mocked(ModuleSetupModal).mockReturnValue(
mockModuleSetupModal
) @@ -118,15 +126,6 @@ describe('SetupModulesList', () => { ) }) - it('should render the list view headers', () => { - when(useRunHasStarted).calledWith(RUN_ID).thenReturn(false) - when(useModuleRenderInfoForProtocolById).calledWith(RUN_ID).thenReturn({}) - render(props) - screen.getByText('Module') - screen.getByText('Location') - screen.getByText('Status') - }) - it('should render a magnetic module that is connected', () => { vi.mocked(useModuleRenderInfoForProtocolById).mockReturnValue({ [mockMagneticModule.moduleId]: { @@ -301,8 +300,13 @@ describe('SetupModulesList', () => { screen.getByText('Connected') }) - it('should render the MoaM component when Moam is attached', () => { - vi.mocked(MultipleModulesModal).mockReturnValue(
mock Moam modal
) + it('should render the MoaM component when Moam is attached and robot is OT2', () => { + when(vi.mocked(useRobot)) + .calledWith(ROBOT_NAME) + .thenReturn({ robotModel: OT2_ROBOT_TYPE } as DiscoveredRobot) + vi.mocked(OT2MultipleModulesHelp).mockReturnValue( +
mock Moam modal
+ ) when(useUnmatchedModulesForProtocol) .calledWith(ROBOT_NAME, RUN_ID) .thenReturn({ @@ -355,8 +359,6 @@ describe('SetupModulesList', () => { }, }) render(props) - const help = screen.getByTestId('Banner_close-button') - fireEvent.click(help) screen.getByText('mock Moam modal') }) it('should render the module unmatching banner', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 4e9afd58604f..f1e06c2471a1 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -7,6 +7,10 @@ import { SPACING, useHoverTooltip, PrimaryButton, + DIRECTION_ROW, + JUSTIFY_SPACE_BETWEEN, + StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' @@ -46,7 +50,7 @@ export const SetupModuleAndDeck = ({ hasModules, protocolAnalysis, }: SetupModuleAndDeckProps): JSX.Element => { - const { t } = useTranslation('protocol_setup') + const { t, i18n } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( t('list_view'), t('map_view') @@ -75,14 +79,51 @@ export const SetupModuleAndDeck = ({ {toggleGroup} {selectedValue === t('list_view') ? ( <> - {hasModules ? ( - - ) : null} - {requiredDeckConfigCompatibility.length > 0 ? ( - - ) : null} + + + {i18n.format(t('deck_hardware'), 'capitalize')} + + + {t('location')} + + + {t('status')} + + + + {hasModules ? ( + + ) : null} + {requiredDeckConfigCompatibility.length > 0 ? ( + + ) : null} + ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 10bf9b5148d6..b0702fccdf9a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -1,5 +1,10 @@ import { + HEATERSHAKER_MODULE_V1_FIXTURE, + MAGNETIC_BLOCK_V1_FIXTURE, STAGING_AREA_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_V2_FIXTURE, + THERMOCYCLER_V2_FRONT_FIXTURE, + THERMOCYCLER_V2_REAR_FIXTURE, TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, @@ -48,6 +53,16 @@ export function getFixtureImage(cutoutFixtureId: CutoutFixtureId): string { return wasteChuteStagingArea } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { return trashBin + } else if (cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE) { + return thermoModuleGen2 + } else if (cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE) { + return thermoModuleGen2 + } else if (cutoutFixtureId === HEATERSHAKER_MODULE_V1_FIXTURE) { + return heaterShakerModule + } else if (cutoutFixtureId === TEMPERATURE_MODULE_V2_FIXTURE) { + return temperatureModule + } else if (cutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { + return magneticBlockGen1 } else { return 'Error: unknown fixture' } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx index 8844f551d08b..f683986c26bc 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunRuntimeParameters.test.tsx @@ -3,7 +3,7 @@ import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { screen } from '@testing-library/react' import { when } from 'vitest-when' -import { NoParameters } from '@opentrons/components' +import { InfoScreen } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -16,10 +16,10 @@ import type { } from '@opentrons/shared-data' vi.mock('@opentrons/components', async importOriginal => { - const actual = await importOriginal() + const actual = await importOriginal() return { ...actual, - NoParameters: vi.fn(), + InfoScreen: vi.fn(), } }) vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') @@ -31,7 +31,7 @@ const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, value: false, }, @@ -94,7 +94,7 @@ describe('ProtocolRunRuntimeParameters', () => { props = { runId: RUN_ID, } - vi.mocked(NoParameters).mockReturnValue(
mock NoParameter
) + vi.mocked(InfoScreen).mockReturnValue(
mock InfoScreen
) when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -106,7 +106,30 @@ describe('ProtocolRunRuntimeParameters', () => { vi.resetAllMocks() }) - it('should render title, and banner when RunTimeParameters are note empty', () => { + it('should render title, and banner when RunTimeParameters are note empty and all values are default', () => { + render(props) + screen.getByText('Parameters') + screen.getByText('Default values') + screen.getByText('Values are view-only') + screen.getByText('Cancel the run and restart setup to edit') + screen.getByText('Name') + screen.getByText('Value') + }) + + it('should render title, and banner when RunTimeParameters are note empty and some value is changed', () => { + vi.mocked(useMostRecentCompletedAnalysis).mockReturnValue({ + runTimeParameters: [ + ...mockRunTimeParameterData, + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'bool', + default: false, + value: true, + }, + ], + } as CompletedProtocolAnalysis) render(props) screen.getByText('Parameters') screen.getByText('Custom values') @@ -128,7 +151,7 @@ describe('ProtocolRunRuntimeParameters', () => { screen.getByText('No offsets') }) - it('should render mock NoParameter component when RunTimeParameters are empty', () => { + it('should render mock InfoScreen component when RunTimeParameters are empty', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith(RUN_ID) .thenReturn({ @@ -137,7 +160,7 @@ describe('ProtocolRunRuntimeParameters', () => { render(props) screen.getByText('Parameters') expect(screen.queryByText('Default values')).not.toBeInTheDocument() - screen.getByText('mock NoParameter') + screen.getByText('mock InfoScreen') }) // ToDo Additional test will be implemented when chip component is added diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts index f96bacc93b64..0da562e9549d 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' -import { transfer_settings, ot2DeckDefV4 } from '@opentrons/shared-data' +import { transfer_settings, ot2DeckDefV5 } from '@opentrons/shared-data' import { getLabwareRenderInfo } from '../getLabwareRenderInfo' import type { CompletedProtocolAnalysis, @@ -8,7 +8,7 @@ import type { } from '@opentrons/shared-data' const protocolWithMagTempTC = (transfer_settings as unknown) as CompletedProtocolAnalysis -const standardDeckDef = ot2DeckDefV4 as any +const standardDeckDef = ot2DeckDefV5 as any describe('getLabwareRenderInfo', () => { it('should gather labware coordinates', () => { diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts index cd6b5d06408a..93528250b0dd 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest' import { transfer_settings, multiple_temp_modules, - ot2DeckDefV4, + ot2DeckDefV5, getModuleDef2, ProtocolAnalysisOutput, LoadedLabware, @@ -174,7 +174,7 @@ const protocolWithMultipleTemps = ({ }, ] as LoadedModule[], } as unknown) as ProtocolAnalysisOutput -const standardDeckDef = ot2DeckDefV4 as any +const standardDeckDef = ot2DeckDefV5 as any describe('getProtocolModulesInfo', () => { it('should gather protocol module info for temp, mag, and tc', () => { diff --git a/app/src/organisms/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Devices/RobotOverflowMenu.tsx index 751729b25a82..dd624e2dcd58 100644 --- a/app/src/organisms/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Devices/RobotOverflowMenu.tsx @@ -84,25 +84,27 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> - {!isRobotBusy ? ( - - {t('run_a_protocol')} - - ) : null} + + {t('run_a_protocol')} + {isRobotOnWrongVersionOfSoftware && ( {t('shared:a_software_update_is_available')} )} + {!isRobotOnWrongVersionOfSoftware && isRobotBusy && ( + + {t('shared:robot_is_busy')} + + )} - {t('connection_lost_description')} + {t('branded:connection_lost_description')} void + robotName: string +} + +interface FormValues { + passwordInput: string +} + +export function FactoryModeSlideout({ + isExpanded, + onCloseClick, + robotName, +}: FactoryModeSlideoutProps): JSX.Element { + const { t } = useTranslation(['device_settings', 'shared', 'branded']) + + const dispatch = useDispatch() + + const { settings } = useRobotSettingsQuery().data ?? {} + const oemModeSetting = (settings ?? []).find( + (setting: RobotSettingsField) => setting?.id === 'enableOEMMode' + ) + const isOEMMode = oemModeSetting?.value ?? null + + const [currentStep, setCurrentStep] = React.useState(1) + const [toggleValue, setToggleValue] = React.useState(false) + + const { + handleSubmit, + control, + formState: { errors }, + trigger, + } = useForm({ + defaultValues: { + passwordInput: '', + }, + }) + const onSubmit = (data: FormValues): void => { + setCurrentStep(2) + } + + const handleSubmitFactoryPassword = (): void => { + // TODO: validation and errors: PLAT-281 + void handleSubmit(onSubmit)() + } + + const handleToggleClick: React.MouseEventHandler = () => { + setToggleValue(toggleValue => !toggleValue) + } + + const handleCompleteClick: React.MouseEventHandler = () => { + dispatch(updateSetting(robotName, 'enableOEMMode', toggleValue)) + dispatch(restartRobot(robotName)) + onCloseClick() + } + + React.useEffect(() => { + // initialize local state to OEM mode value + if (isOEMMode != null) { + setToggleValue(isOEMMode) + } + }, [isOEMMode]) + + return ( + + {currentStep === 1 ? ( + + {t('shared:next')} + + ) : null} + {currentStep === 2 ? ( + + {t('complete_and_restart_robot')} + + ) : null} + + } + > + {currentStep === 1 ? ( + + ( + ) => { + field.onChange(e) + trigger('passwordInput') + }} + value={field.value} + error={fieldState.error?.message && ' '} + onBlur={field.onBlur} + title={t('enter_factory_password')} + /> + )} + /> + {errors.passwordInput != null ? ( + + {errors.passwordInput.message} + + ) : null} + + ) : null} + {currentStep === 2 ? ( + + + {t('oem_mode')} + + + + + {toggleValue ? t('on') : t('off')} + + + {t('branded:oem_mode_description')} + + ) : null} + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx index 63cfd490c51f..b741f3ef5c84 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/__tests__/DeviceResetModal.test.tsx @@ -103,7 +103,7 @@ describe('RobotSettings DeviceResetModal', () => { }) screen.getByText('Connection to robot lost') screen.getByText( - 'The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wifi connection to the robot, then try to reconnect.' + 'The Opentrons App is unable to communicate with this robot right now. Double check the USB or Wi-Fi connection to the robot, then try to reconnect.' ) screen.getByRole('button', { name: 'close' }) }) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx new file mode 100644 index 000000000000..8d2fda7c3867 --- /dev/null +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/FactoryMode.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + Box, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING_AUTO, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { TertiaryButton } from '../../../../atoms/buttons' + +interface FactoryModeProps { + isRobotBusy: boolean + setShowFactoryModeSlideout: React.Dispatch> +} + +export function FactoryMode({ + isRobotBusy, + setShowFactoryModeSlideout, +}: FactoryModeProps): JSX.Element { + const { t } = useTranslation('device_settings') + + return ( + + + + {t('factory_mode')} + + + { + setShowFactoryModeSlideout(true) + }} + > + {t('setup_mode')} + + + ) +} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx index 193b0e140b92..0be4e872ed44 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/RobotServerVersion.tsx @@ -32,7 +32,7 @@ const GITHUB_LINK = export function RobotServerVersion({ robotName, }: RobotServerVersionProps): JSX.Element { - const { t } = useTranslation(['device_settings', 'shared']) + const { t } = useTranslation(['device_settings', 'shared', 'branded']) const robot = useRobot(robotName) const isFlex = useIsFlex(robotName) const { autoUpdateAction } = useSelector((state: State) => { @@ -65,7 +65,7 @@ export function RobotServerVersion({ {isFlex ? ( - {t('robot_server_version_ot3_description')} + {t('branded:robot_server_version_ot3_description')} ) : null} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx deleted file mode 100644 index 9884676e224d..000000000000 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/SoftwareUpdateModal.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { - DIRECTION_COLUMN, - Flex, - JUSTIFY_FLEX_END, - PrimaryButton, - SecondaryButton, - SPACING, - StyledText, - TYPOGRAPHY, -} from '@opentrons/components' -import { getShellUpdateState } from '../../../../redux/shell' -import { useCurrentRunId } from '../../../../organisms/ProtocolUpload/hooks' -// import { ReleaseNotes } from '../../../../molecules/ReleaseNotes' - -import { ExternalLink } from '../../../../atoms/Link/ExternalLink' -import { Banner } from '../../../../atoms/Banner' -import { LegacyModal } from '../../../../molecules/LegacyModal' -import { CONNECTABLE, REACHABLE } from '../../../../redux/discovery' -import { Divider } from '../../../../atoms/structure' -import { useRobot } from '../../hooks' -import { handleUpdateBuildroot } from '../UpdateBuildroot' - -const TECHNICAL_CHANGE_LOG_URL = - 'https://github.com/Opentrons/opentrons/blob/edge/CHANGELOG.md' -const ISSUE_TRACKER_URL = - 'https://github.com/Opentrons/opentrons/issues?q=is%3Aopen+is%3Aissue+label%3Abug' -const RELEASE_NOTES_URL = 'https://github.com/Opentrons/opentrons/releases' - -interface SoftwareUpdateModalProps { - robotName: string - closeModal: () => void -} - -export function SoftwareUpdateModal({ - robotName, - closeModal, -}: SoftwareUpdateModalProps): JSX.Element | null { - const { t } = useTranslation('device_settings') - - const currentRunId = useCurrentRunId() - // ToDo: Add release notes for the new design - const updateState = useSelector(getShellUpdateState) - // const { downloaded, downloading, error, info: updateInfo } = updateState - const { info: updateInfo } = updateState - const version = updateInfo?.version ?? '' - // const releaseNotes = updateInfo?.releaseNotes - const [showUpdateModal, setShowUpdateModal] = React.useState(false) - const robot = useRobot(robotName) - - if (robot?.status !== CONNECTABLE && robot?.status !== REACHABLE) return null - - return !showUpdateModal ? ( - - {t('requires_restarting_the_robot')} - - {/* ToDo: align with new design */} - - {t('app_change_in', { version })} - - - {'None in the Opentrons (Here will be change logs)'} - - - {t('new_features')} - - - {'None in the Opentrons (Here will be features info)'} - - - {t('bug_fixes')} - - - {'None in the Opentrons (Here will be fixes info)'} - - - - {t('view_opentrons_technical_change_log')} - - - {t('view_opentrons_issue_tracker')} - - - {t('view_opentrons_release_notes')} - - - - {t('remind_me_later')} - - { - setShowUpdateModal(true) - handleUpdateBuildroot(robot) - }} - disabled={currentRunId != null} - > - {t('update_robot_now')} - - - - - ) : null -} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx index a8febac70929..bf7a27e389b2 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/UpdateRobotSoftware.tsx @@ -40,7 +40,7 @@ export function UpdateRobotSoftware({ onUpdateStart, isRobotBusy, }: UpdateRobotSoftwareProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const { updateFromFileDisabledReason } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robotName) }) @@ -77,10 +77,10 @@ export function UpdateRobotSoftware({ {t('update_robot_software')} - {t('update_robot_software_description')} + {t('branded:update_robot_software_description')} - {t('update_robot_software_link')} + {t('branded:update_robot_software_link')} () const value = settings?.value ? settings.value : false const id = settings?.id ? settings.id : 'disableFastProtocolUpload' @@ -54,7 +54,7 @@ export function UseOlderProtocol({ {t('use_older_protocol_analysis_method')} - {t('use_older_protocol_analysis_method_description')} + {t('branded:use_older_protocol_analysis_method_description')} { - const actual = await importOriginal() - return { - ...actual, - getShellUpdateState: vi.fn(), - } -}) -vi.mock('../../../hooks') -vi.mock('../../../../../redux/discovery/selectors') - -const mockClose = vi.fn() - -const render = () => { - return renderWithProviders( - - - , - { i18nInstance: i18n } - ) -} - -describe('RobotSettings SoftwareUpdateModal', () => { - beforeEach(() => { - vi.mocked(useRobot).mockReturnValue(mockReachableRobot) - vi.mocked(getShellUpdateState).mockReturnValue({ - downloaded: true, - info: { - version: '1.2.3', - releaseNotes: 'this is a release', - }, - } as ShellUpdateState) - }) - - it('should render title ,description and button', () => { - render() - screen.getByText('Robot Update Available') - screen.getByText( - 'Updating the robot’s software requires restarting the robot' - ) - screen.getByText('App Changes in 1.2.3') - screen.getByText('New Features') - screen.getByText('Bug Fixes') - screen.getByText('View Opentrons technical change log') - screen.getByText('View Opentrons issue tracker') - screen.getByText('View full Opentrons release notes') - screen.getByRole('button', { name: 'Remind me later' }) - screen.getByRole('button', { name: 'Update robot now' }) - }) - - it('should have correct href', () => { - render() - const changeLogUrl = - 'https://github.com/Opentrons/opentrons/blob/edge/CHANGELOG.md' - const issueTrackerUrl = - 'https://github.com/Opentrons/opentrons/issues?q=is%3Aopen+is%3Aissue+label%3Abug' - const releaseNotesUrl = 'https://github.com/Opentrons/opentrons/releases' - - const linkForChangeLog = screen.getByRole('link', { - name: 'View Opentrons technical change log', - }) - expect(linkForChangeLog).toHaveAttribute('href', changeLogUrl) - - const linkForIssueTracker = screen.getByRole('link', { - name: 'View Opentrons issue tracker', - }) - expect(linkForIssueTracker.closest('a')).toHaveAttribute( - 'href', - issueTrackerUrl - ) - - const linkForReleaseNotes = screen.getByRole('link', { - name: 'View full Opentrons release notes', - }) - expect(linkForReleaseNotes.closest('a')).toHaveAttribute( - 'href', - releaseNotesUrl - ) - }) -}) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts index 1c5cb506bc74..b53134df9458 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/index.ts @@ -1,13 +1,13 @@ export * from './DeviceReset' export * from './DisplayRobotName' export * from './EnableStatusLight' +export * from './FactoryMode' export * from './GantryHoming' export * from './LegacySettings' export * from './OpenJupyterControl' export * from './RobotInformation' export * from './RobotServerVersion' export * from './ShortTrashBin' -export * from './SoftwareUpdateModal' export * from './Troubleshooting' export * from './UpdateRobotSoftware' export * from './UsageSettings' diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx index 660e04a15192..b489af43d1f9 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/DisconnectModal.tsx @@ -47,7 +47,7 @@ export const DisconnectModal = ({ onCancel, robotName, }: DisconnectModalProps): JSX.Element => { - const { t } = useTranslation(['device_settings', 'shared']) + const { t } = useTranslation(['device_settings', 'shared', 'branded']) const wifiList = useWifiList(robotName) const { wifi } = useSelector((state: State) => @@ -144,7 +144,7 @@ export const DisconnectModal = ({ {isError ? ( - {t('shared:general_error_message')} + {t('branded:general_error_message')} ) : null} diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx index adf1e9a591a8..79823d81ef3f 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/DisconnectModal.test.tsx @@ -160,7 +160,7 @@ describe('DisconnectModal', () => { 'Your robot was unable to disconnect from Wi-Fi network foo.' ) screen.getByText( - 'If you keep getting this message, try restarting your app and/or robot. If this does not resolve the issue please contact Opentrons Support.' + 'If you keep getting this message, try restarting your app and robot. If this does not resolve the issue, contact Opentrons Support.' ) screen.getByRole('button', { name: 'cancel' }) screen.getByRole('button', { name: 'Disconnect' }) diff --git a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx index 8772f9a383ad..be9cdcd2be4f 100644 --- a/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -19,6 +19,7 @@ import { DeviceReset, DisplayRobotName, EnableStatusLight, + FactoryMode, GantryHoming, LegacySettings, OpenJupyterControl, @@ -39,6 +40,7 @@ import { import { RenameRobotSlideout } from './AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout' import { DeviceResetSlideout } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout' import { DeviceResetModal } from './AdvancedTab/AdvancedTabSlideouts/DeviceResetModal' +import { FactoryModeSlideout } from './AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout' import { handleUpdateBuildroot } from './UpdateBuildroot' import { UNREACHABLE } from '../../../redux/discovery' import { getTopPortalEl } from '../../../App/portal' @@ -72,6 +74,10 @@ export function RobotSettingsAdvanced({ showDeviceResetModal, setShowDeviceResetModal, ] = React.useState(false) + const [ + showFactoryModeSlideout, + setShowFactoryModeSlideout, + ] = React.useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) @@ -131,6 +137,13 @@ export function RobotSettingsAdvanced({ robotName={robotName} /> )} + {showFactoryModeSlideout && ( + setShowFactoryModeSlideout(false)} + robotName={robotName} + /> + )} {showDeviceResetSlideout && ( handleUpdateBuildroot(robot)} /> + {isFlex ? ( + <> + + + + ) : null} ((state: State) => getRobotSettings(state, robotName) ) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index f02ad6ae3cec..4a1ffaec5ea4 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -155,7 +155,7 @@ export function UpdateRobotModal({ > - {t('update_requires_restarting')} + {t('update_requires_restarting_robot')} diff --git a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx index bc59f8cf884b..dccbb3dfefca 100644 --- a/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx +++ b/app/src/organisms/Devices/__tests__/HistoricalProtocolRun.test.tsx @@ -18,8 +18,8 @@ vi.mock('../../../redux/protocol-storage') vi.mock('../../RunTimeControl/hooks') vi.mock('../HistoricalProtocolRunOverflowMenu') vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = importOriginal() - return await { + const reactRouterDom = await importOriginal() + return { ...reactRouterDom, useHistory: () => ({ push: mockPush } as any), } diff --git a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx index 69ad5afc77a2..6227cbd5675f 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverflowMenu.test.tsx @@ -20,6 +20,7 @@ vi.mock('../../../redux/robot-update/selectors') vi.mock('../../ProtocolUpload/hooks') vi.mock('../../ChooseProtocolSlideout') vi.mock('../hooks') +vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') const render = (props: React.ComponentProps) => { return renderWithProviders( @@ -85,19 +86,13 @@ describe('RobotOverflowMenu', () => { expect(run).toBeDisabled() }) - it('should only render robot settings when e-stop is pressed or disconnected', () => { + it('disables the run a protocol menu item if robot is busy', () => { vi.mocked(useCurrentRunId).mockReturnValue(null) - vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ - autoUpdateAction: 'upgrade', - autoUpdateDisabledReason: null, - updateFromFileDisabledReason: null, - }) - vi.mocked(useIsRobotBusy).mockReturnValue(true) render(props) const btn = screen.getByLabelText('RobotOverflowMenu_button') fireEvent.click(btn) - expect(screen.queryByText('Run a protocol')).not.toBeInTheDocument() - screen.getByText('Robot settings') + const run = screen.getByText('Run a protocol') + expect(run).toBeDisabled() }) }) diff --git a/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx b/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx index b02e5ce600a9..66f6d18b7d07 100644 --- a/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotOverview.test.tsx @@ -51,8 +51,8 @@ import type { State } from '../../../redux/types' import type * as ReactApiClient from '@opentrons/react-api-client' vi.mock('@opentrons/react-api-client', async importOriginal => { - const actual = importOriginal() - return await { + const actual = await importOriginal() + return { ...actual, useAuthorization: vi.fn(), } diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index 11b744f57a27..540b15327990 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -1,10 +1,11 @@ import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { UseQueryResult } from 'react-query' import { - STAGING_AREA_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, + TEMPERATURE_MODULE_V2_FIXTURE, heater_shaker_commands_with_results_key, } from '@opentrons/shared-data' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -13,7 +14,6 @@ import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { getProtocolModulesInfo } from '../../ProtocolRun/utils/getProtocolModulesInfo' import { - mockMagneticModuleGen2, mockTemperatureModuleGen2, mockThermocycler, } from '../../../../redux/modules/__fixtures__' @@ -30,6 +30,8 @@ import type { ModuleType, ProtocolAnalysisOutput, } from '@opentrons/shared-data' +import type { UseQueryResult } from 'react-query' +import type { AttachedModule } from '../../../../redux/modules/types' vi.mock('@opentrons/react-api-client') vi.mock('../../ProtocolRun/utils/getProtocolModulesInfo') @@ -53,25 +55,28 @@ const PROTOCOL_DETAILS = { protocolKey: 'fakeProtocolKey', } -const mockMagneticModuleDefinition = { - moduleId: 'someMagneticModule', - model: 'magneticModuleV2' as ModuleModel, - type: 'magneticModuleType' as ModuleType, - labwareOffset: { x: 5, y: 5, z: 5 }, - cornerOffsetFromSlot: { x: 1, y: 1, z: 1 }, - dimensions: { - xDimension: 100, - yDimension: 100, - footprintXDimension: 50, - footprintYDimension: 50, - labwareInterfaceXDimension: 80, - labwareInterfaceYDimension: 120, +const mockAttachedTempMod: AttachedModule = { + id: 'temp_mod_1', + moduleModel: TEMPERATURE_MODULE_V2, + moduleType: TEMPERATURE_MODULE_TYPE, + serialNumber: 'abc123', + hardwareRevision: 'heatershaker_v4.0', + firmwareVersion: 'v2.0.0', + hasAvailableUpdate: true, + data: { + currentTemperature: 40, + targetTemperature: null, + status: 'idle', + }, + usbPort: { + path: '/dev/ot_module_heatershaker0', + port: 1, + portGroup: 'unknown', + hub: false, }, - twoDimensionalRendering: { children: [] }, } const mockTemperatureModuleDefinition = { - moduleId: 'someMagneticModule', model: 'temperatureModuleV2' as ModuleModel, type: 'temperatureModuleType' as ModuleType, labwareOffset: { x: 5, y: 5, z: 5 }, @@ -87,19 +92,6 @@ const mockTemperatureModuleDefinition = { twoDimensionalRendering: { children: [] }, } -const MAGNETIC_MODULE_INFO = { - moduleId: 'magneticModuleId', - x: 0, - y: 0, - z: 0, - moduleDef: mockMagneticModuleDefinition as any, - nestedLabwareDef: null, - nestedLabwareId: null, - nestedLabwareDisplayName: null, - protocolLoadOrder: 0, - slotName: 'D1', -} - const TEMPERATURE_MODULE_INFO = { moduleId: 'temperatureModuleId', x: 0, @@ -111,11 +103,12 @@ const TEMPERATURE_MODULE_INFO = { nestedLabwareDisplayName: null, protocolLoadOrder: 0, slotName: 'D1', -} +} as any const mockCutoutConfig: CutoutConfig = { cutoutId: 'cutoutD1', - cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: 'abc123', } describe('useModuleRenderInfoForProtocolById hook', () => { @@ -123,8 +116,8 @@ describe('useModuleRenderInfoForProtocolById hook', () => { vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [mockCutoutConfig], } as UseQueryResult) + vi.mocked(useAttachedModules).mockReturnValue([mockAttachedTempMod]) vi.mocked(useAttachedModules).mockReturnValue([ - mockMagneticModuleGen2, mockTemperatureModuleGen2, mockThermocycler, ]) @@ -134,10 +127,7 @@ describe('useModuleRenderInfoForProtocolById hook', () => { when(vi.mocked(useMostRecentCompletedAnalysis)) .calledWith('1') .thenReturn(PROTOCOL_DETAILS.protocolData as any) - vi.mocked(getProtocolModulesInfo).mockReturnValue([ - TEMPERATURE_MODULE_INFO, - MAGNETIC_MODULE_INFO, - ]) + vi.mocked(getProtocolModulesInfo).mockReturnValue([TEMPERATURE_MODULE_INFO]) }) it('should return no module render info when protocol details not found', () => { @@ -155,13 +145,8 @@ describe('useModuleRenderInfoForProtocolById hook', () => { useModuleRenderInfoForProtocolById('1', true) ) expect(result.current).toStrictEqual({ - magneticModuleId: { - conflictedFixture: mockCutoutConfig, - attachedModuleMatch: mockMagneticModuleGen2, - ...MAGNETIC_MODULE_INFO, - }, temperatureModuleId: { - conflictedFixture: mockCutoutConfig, + conflictedFixture: null, attachedModuleMatch: mockTemperatureModuleGen2, ...TEMPERATURE_MODULE_INFO, }, diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 34365a075e7b..fa63db104c63 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -42,6 +42,8 @@ const modifiedStoredProtocolData = { commands: storedProtocolData?.mostRecentAnalysis?.commands, liquids: storedProtocolData?.mostRecentAnalysis?.liquids, errors: storedProtocolData?.mostRecentAnalysis?.errors, + runTimeParameters: + storedProtocolData?.mostRecentAnalysis?.runTimeParameters, }, } diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 6ca57b24c4ae..e606e846d533 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,12 +1,10 @@ import { checkModuleCompatibility, FLEX_ROBOT_TYPE, - getCutoutIdForSlotName, + getCutoutFixturesForModuleModel, + getCutoutIdsFromModuleSlotName, getDeckDefFromRobotType, - MAGNETIC_BLOCK_TYPE, - SINGLE_SLOT_FIXTURES, - STAGING_AREA_RIGHT_SLOT_FIXTURE, - THERMOCYCLER_MODULE_TYPE, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' @@ -35,7 +33,7 @@ export function useModuleRenderInfoForProtocolById( pollModules?: boolean ): ModuleRenderInfoById { const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) - const { data: deckConfig } = useDeckConfigurationQuery({ + const { data: deckConfig = [] } = useDeckConfigurationQuery({ refetchInterval: REFETCH_INTERVAL_5000_MS, }) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) @@ -45,50 +43,57 @@ export function useModuleRenderInfoForProtocolById( }) if (protocolAnalysis == null) return {} - const deckDef = getDeckDefFromRobotType( - protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE - ) + const assumedRobotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE + const deckDef = getDeckDefFromRobotType(assumedRobotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) const protocolModulesInfoInLoadOrder = protocolModulesInfo.sort( (modA, modB) => modA.protocolLoadOrder - modB.protocolLoadOrder ) + + const robotSupportsModuleConfig = assumedRobotType !== OT2_ROBOT_TYPE let matchedAmod: AttachedModule[] = [] const allModuleRenderInfo = protocolModulesInfoInLoadOrder.map( protocolMod => { + const moduleFixtures = getCutoutFixturesForModuleModel( + protocolMod.moduleDef.model, + deckDef + ) + const moduleCutoutIds = getCutoutIdsFromModuleSlotName( + protocolMod.slotName, + moduleFixtures, + deckDef + ) const compatibleAttachedModule = attachedModules.find( attachedMod => + // first check module model compatibility checkModuleCompatibility( attachedMod.moduleModel, protocolMod.moduleDef.model - ) && !matchedAmod.find(m => m === attachedMod) + ) && + // then check that the module hasn't already been matched + !matchedAmod.some( + m => m.serialNumber === attachedMod.serialNumber + ) && + // then if robotType supports configurable modules check the deck config has a + // a module with the expected serial number in the expected location + (!robotSupportsModuleConfig || + deckConfig.some( + ({ cutoutId, opentronsModuleSerialNumber }) => + attachedMod.serialNumber === opentronsModuleSerialNumber && + moduleCutoutIds.includes(cutoutId) + )) ) ?? null - const cutoutIdForSlotName = getCutoutIdForSlotName( - protocolMod.slotName, - deckDef - ) - - const isMagneticBlockModule = - protocolMod.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE - - const isThermocycler = - protocolMod.moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE - const conflictedFixture = deckConfig?.find( - fixture => - (fixture.cutoutId === cutoutIdForSlotName || - // special-case A1 for the thermocycler to require a single slot fixture - (isThermocycler && fixture.cutoutId === 'cutoutA1')) && - fixture.cutoutFixtureId != null && - // do not generate a conflict for single slot fixtures, because modules are not yet fixtures - !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) && - // special case the magnetic module because unlike other modules it sits in a slot that can also be provided by a staging area fixture - (!isMagneticBlockModule || - fixture.cutoutFixtureId !== STAGING_AREA_RIGHT_SLOT_FIXTURE) + ({ cutoutId, cutoutFixtureId }) => + moduleCutoutIds.includes(cutoutId) && + !moduleFixtures.some(({ id }) => cutoutFixtureId === id) && + // if robotType supports module config, don't treat module fixture as conflict + (!robotSupportsModuleConfig || compatibleAttachedModule == null) ) ?? null if (compatibleAttachedModule !== null) { diff --git a/app/src/organisms/Devices/utils.ts b/app/src/organisms/Devices/utils.ts index a4d72e0d2792..61c133f176bb 100644 --- a/app/src/organisms/Devices/utils.ts +++ b/app/src/organisms/Devices/utils.ts @@ -9,7 +9,9 @@ import type { Instruments, PipetteData, PipetteOffsetCalibration, + RunTimeParameterCreateData, } from '@opentrons/api-client' +import type { RunTimeParameter } from '@opentrons/shared-data' /** * formats a string if it is in ISO 8601 date format @@ -89,3 +91,15 @@ export function getShowPipetteCalibrationWarning( }) ?? false ) } + +export function getRunTimeParameterValuesForRun( + runTimeParameters: RunTimeParameter[] +): RunTimeParameterCreateData { + return runTimeParameters.reduce( + (acc, param) => + param.value !== param.default + ? { ...acc, [param.variableName]: param.value } + : acc, + {} + ) +} diff --git a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx index 69a8d7de6947..cd21cc3e1a40 100644 --- a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx +++ b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx @@ -24,30 +24,20 @@ import { import { SmallButton, MediumButton } from '../../atoms/buttons' import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -// import { NeedHelpLink } from '../CalibrationPanels' import blowoutVideo from '../../assets/videos/droptip-wizard/Blowout-Liquid.webm' import droptipVideo from '../../assets/videos/droptip-wizard/Drop-tip.webm' -// TODO: get help link article URL -// const NEED_HELP_URL = '' - interface BeforeBeginningProps { setShouldDispenseLiquid: (shouldDispenseLiquid: boolean) => void createdMaintenanceRunId: string | null isOnDevice: boolean - isRobotMoving: boolean } export const BeforeBeginning = ( props: BeforeBeginningProps ): JSX.Element | null => { - const { - setShouldDispenseLiquid, - createdMaintenanceRunId, - isOnDevice, - isRobotMoving, - } = props + const { setShouldDispenseLiquid, createdMaintenanceRunId, isOnDevice } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const [flowType, setFlowType] = React.useState< 'liquid_and_tips' | 'only_tips' | null @@ -57,16 +47,8 @@ export const BeforeBeginning = ( setShouldDispenseLiquid(flowType === 'liquid_and_tips') } - if (isRobotMoving || createdMaintenanceRunId == null) { - return ( - - ) + if (createdMaintenanceRunId == null) { + return } if (isOnDevice) { diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx index 8050c7766982..7a86da67223b 100644 --- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx +++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx @@ -22,15 +22,13 @@ import { import { getDeckDefFromRobotType } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -// import { NeedHelpLink } from '../CalibrationPanels' import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout' import type { CommandData } from '@opentrons/api-client' import type { AddressableAreaName, RobotType } from '@opentrons/shared-data' +import type { ErrorDetails } from './utils' // TODO: get help link article URL -// const NEED_HELP_URL = '' interface ChooseLocationProps { handleProceed: () => void @@ -41,9 +39,8 @@ interface ChooseLocationProps { moveToAddressableArea: ( addressableArea: AddressableAreaName ) => Promise - isRobotMoving: boolean isOnDevice: boolean - setErrorMessage: (arg0: string) => void + setErrorDetails: (errorDetails: ErrorDetails) => void } export const ChooseLocation = ( @@ -56,9 +53,8 @@ export const ChooseLocation = ( body, robotType, moveToAddressableArea, - isRobotMoving, isOnDevice, - setErrorMessage, + setErrorDetails, } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const deckDef = getDeckDefFromRobotType(robotType) @@ -74,14 +70,10 @@ export const ChooseLocation = ( if (deckSlot != null) { moveToAddressableArea(deckSlot) .then(() => handleProceed()) - .catch(e => setErrorMessage(`${e.message}`)) + .catch(e => setErrorDetails({ message: `${e.message}` })) } } - if (isRobotMoving) { - return - } - if (isOnDevice) { return ( void handleGoBack: () => void - isRobotMoving: boolean } export function ExitConfirmation(props: ExitConfirmationProps): JSX.Element { - const { handleGoBack, handleExit, isRobotMoving } = props + const { handleGoBack, handleExit } = props const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) const flowTitle = t('drop_tips') const isOnDevice = useSelector(getIsOnDevice) - if (isRobotMoving) { - return - } - return ( void body: string - isRobotMoving: boolean currentStep: string isOnDevice: boolean } @@ -161,7 +160,6 @@ export const JogToPosition = ( handleJog, handleProceed, body, - isRobotMoving, currentStep, isOnDevice, } = props @@ -171,10 +169,10 @@ export const JogToPosition = ( setShowPositionConfirmation, ] = React.useState(false) // Includes special case homing only present in this step. - const [isRobotInMotion, setIsRobotInMotion] = React.useState(isRobotMoving) + const [isRobotInMotion, setIsRobotInMotion] = React.useState(false) const onGoBack = (): void => { - setIsRobotInMotion(() => true) + setIsRobotInMotion(true) handleGoBack() } @@ -201,11 +199,6 @@ export const JogToPosition = ( ) } - // Moving due to "Exit" or "Go back" click. - if (isRobotInMotion) { - return - } - if (isOnDevice) { return ( { + let props: UseDropTipErrorComponentsProps + let mockOnClose: Mock + let mockTranslation: Mock + let mockChainRunCommands: Mock + + beforeEach(() => { + mockOnClose = vi.fn() + mockTranslation = vi.fn() + mockChainRunCommands = vi.fn() + + props = { + maintenanceRunId: MOCK_MAINTENANCE_RUN_ID, + onClose: mockOnClose, + errorDetails: { + type: MOCK_ERROR_TYPE, + message: MOCK_ERROR_MESSAGE, + header: MOCK_ERROR_HEADER, + }, + isOnDevice: true, + t: mockTranslation, + chainRunCommands: mockChainRunCommands, + } + }) + + it('should return the generic text and error message if there is are no special-cased error details', () => { + const result = useDropTipErrorComponents(props) + expect(result.button).toBeNull() + render(result.subHeader) + expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed') + screen.getByText(MOCK_ERROR_MESSAGE) + }) + + it('should return a generic message only if there are no error details', () => { + props.errorDetails = null + const result = useDropTipErrorComponents(props) + expect(result.button).toBeNull() + render(result.subHeader) + expect(mockTranslation).toHaveBeenCalledWith('drop_tip_failed') + expect(screen.queryByText(MOCK_ERROR_MESSAGE)).not.toBeInTheDocument() + }) + + it(`should return correct special components if error type is ${DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR}`, () => { + // @ts-expect-error errorDetails is in fact not null in the test. + props.errorDetails.type = DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + const result = useDropTipErrorComponents(props) + expect(mockTranslation).toHaveBeenCalledWith('confirm_removal_and_home') + + render(result.button) + const btn = screen.getByRole('button') + fireEvent.click(btn) + expect(mockOnClose).toHaveBeenCalled() + expect(mockChainRunCommands).toHaveBeenCalledWith( + MOCK_MAINTENANCE_RUN_ID, + [ + { + commandType: 'home' as const, + params: {}, + }, + ], + true + ) + + render(result.subHeader) + screen.getByText(MOCK_ERROR_MESSAGE) + }) +}) + +describe('useWizardExitHeader', () => { + let props: UseWizardExitHeaderProps + let mockHandleCleanUpAndClose: Mock + let mockConfirmExit: Mock + + beforeEach(() => { + mockHandleCleanUpAndClose = vi.fn() + mockConfirmExit = vi.fn() + + props = { + isFinalStep: true, + hasInitiatedExit: false, + errorDetails: null, + handleCleanUpAndClose: mockHandleCleanUpAndClose, + confirmExit: mockConfirmExit, + } + }) + + it('should appropriately return handleCleanUpAndClose', () => { + const handleExit = useWizardExitHeader(props) + expect(handleExit).toEqual(props.handleCleanUpAndClose) + }) + + it('should appropriately return confirmExit', () => { + props = { ...props, isFinalStep: false } + const handleExit = useWizardExitHeader(props) + expect(handleExit).toEqual(props.confirmExit) + }) + + it('should appropriately return handleCleanUpAndClose with homeOnError = false', () => { + const errorDetails = { message: 'Some error occurred' } + const modifiedProps = { ...props, errorDetails } + const handleExit = useWizardExitHeader(modifiedProps) + expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0) + handleExit() + expect(mockHandleCleanUpAndClose).toHaveBeenCalledWith(false) + }) + + it('should appropriately return a function that does nothing ', () => { + const modifiedProps = { ...props, hasInitiatedExit: true } + const handleExit = useWizardExitHeader(modifiedProps) + handleExit() + expect(mockHandleCleanUpAndClose.mock.calls.length).toBe(0) + expect(mockConfirmExit.mock.calls.length).toBe(0) + }) +}) diff --git a/app/src/organisms/DropTipWizard/constants.ts b/app/src/organisms/DropTipWizard/constants.ts index 0390fd1870f0..6d322f779ece 100644 --- a/app/src/organisms/DropTipWizard/constants.ts +++ b/app/src/organisms/DropTipWizard/constants.ts @@ -16,3 +16,7 @@ export const DROP_TIP_STEPS = [ POSITION_AND_DROP_TIP, DROP_TIP_SUCCESS, ] + +export const DROP_TIP_SPECIAL_ERROR_TYPES = { + MUST_HOME_ERROR: 'MustHomeError', +} as const diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx index 3d7896663d45..ca668cd70133 100644 --- a/app/src/organisms/DropTipWizard/index.tsx +++ b/app/src/organisms/DropTipWizard/index.tsx @@ -11,6 +11,7 @@ import { COLORS, BORDERS, StyledText, + JUSTIFY_FLEX_END, } from '@opentrons/components' import { useCreateMaintenanceCommandMutation, @@ -43,6 +44,12 @@ import { BeforeBeginning } from './BeforeBeginning' import { ChooseLocation } from './ChooseLocation' import { JogToPosition } from './JogToPosition' import { Success } from './Success' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' +import { + useHandleDropTipCommandErrors, + useDropTipErrorComponents, + useWizardExitHeader, +} from './utils' import type { PipetteData } from '@opentrons/api-client' import type { CreateMaintenanceRunType } from '@opentrons/react-api-client' @@ -54,6 +61,8 @@ import type { } from '@opentrons/shared-data' import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' import type { Jog } from '../../molecules/JogControls' +import type { ErrorDetails } from './utils' +import type { DropTipWizardStep } from './types' const RUN_REFETCH_INTERVAL_MS = 5000 const JOG_COMMAND_TIMEOUT_MS = 10000 @@ -110,7 +119,7 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { }) .catch(e => e) }, - onError: error => setErrorMessage(error.message), + onError: error => setErrorDetails({ message: error.message }), }) const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ @@ -141,14 +150,16 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { ]) const [isExiting, setIsExiting] = React.useState(false) - const [errorMessage, setErrorMessage] = React.useState(null) + const [errorDetails, setErrorDetails] = React.useState( + null + ) const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation({ onSuccess: () => closeFlow(), onError: () => closeFlow(), }) - const handleCleanUpAndClose = (): void => { + const handleCleanUpAndClose = (homeOnExit: boolean = true): void => { if (hasCleanedUpAndClosed.current) return hasCleanedUpAndClosed.current = true @@ -156,23 +167,23 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { if (maintenanceRunData?.data.id == null) { closeFlow() } else { - chainRunCommands( - maintenanceRunData?.data.id, - [ - { - commandType: 'home' as const, - params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, - }, - ], - true + ;(homeOnExit + ? chainRunCommands( + maintenanceRunData?.data.id, + [ + { + commandType: 'home' as const, + params: { axes: ['leftZ', 'rightZ', 'x', 'y'] }, + }, + ], + true + ) + : new Promise((resolve, reject) => resolve()) ) - .then(() => { - deleteMaintenanceRun(maintenanceRunData?.data.id) - }) .catch(error => { console.error(error.message) - deleteMaintenanceRun(maintenanceRunData?.data.id) }) + .finally(() => deleteMaintenanceRun(maintenanceRunData?.data.id)) } } @@ -188,8 +199,8 @@ export function DropTipWizard(props: MaintenanceRunManagerProps): JSX.Element { handleCleanUpAndClose={handleCleanUpAndClose} chainRunCommands={chainRunCommands} createRunCommand={createMaintenanceCommand} - errorMessage={errorMessage} - setErrorMessage={setErrorMessage} + errorDetails={errorDetails} + setErrorDetails={setErrorDetails} isExiting={isExiting} deckConfig={deckConfig} /> @@ -203,9 +214,9 @@ interface DropTipWizardProps { createMaintenanceRun: CreateMaintenanceRunType isRobotMoving: boolean isExiting: boolean - setErrorMessage: (message: string | null) => void - errorMessage: string | null - handleCleanUpAndClose: () => void + setErrorDetails: (errorDetails: ErrorDetails) => void + errorDetails: ErrorDetails | null + handleCleanUpAndClose: (homeOnError?: boolean) => void chainRunCommands: ReturnType< typeof useChainMaintenanceCommands >['chainRunCommands'] @@ -227,20 +238,23 @@ export const DropTipWizardComponent = ( chainRunCommands, isRobotMoving, createRunCommand, - setErrorMessage, - errorMessage, + setErrorDetails, + errorDetails, isExiting, createdMaintenanceRunId, instrumentModelSpecs, deckConfig, } = props - const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation('drop_tip_wizard') - const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const [shouldDispenseLiquid, setShouldDispenseLiquid] = React.useState< boolean | null >(null) + const hasInitiatedExit = React.useRef(false) + + const isOnDevice = useSelector(getIsOnDevice) + const setSpecificErrorDetails = useHandleDropTipCommandErrors(setErrorDetails) + const DropTipWizardSteps = getDropTipWizardSteps(shouldDispenseLiquid) const currentStep = shouldDispenseLiquid != null @@ -248,11 +262,31 @@ export const DropTipWizardComponent = ( : null const isFinalStep = currentStepIndex === DropTipWizardSteps.length - 1 + const { + confirm: confirmExit, + showConfirmation: showConfirmExit, + cancel: cancelExit, + } = useConditionalConfirm(handleCleanUpAndClose, true) + + const { + button: errorExitBtn, + subHeader: errorSubHeader, + } = useDropTipErrorComponents({ + t, + errorDetails, + isOnDevice, + chainRunCommands, + maintenanceRunId: createdMaintenanceRunId, + onClose: handleCleanUpAndClose, + }) + React.useEffect(() => { if (createdMaintenanceRunId == null) { - createMaintenanceRun({}).catch((e: Error) => - setErrorMessage(`Error creating maintenance run: ${e.message}`) - ) + createMaintenanceRun({}).catch((e: Error) => { + setSpecificErrorDetails({ + message: `Error creating maintenance run: ${e.message}`, + }) + }) } }, []) @@ -280,18 +314,14 @@ export const DropTipWizardComponent = ( }, waitUntilComplete: true, timeout: JOG_COMMAND_TIMEOUT_MS, - }).catch((e: Error) => - setErrorMessage(`Error issuing jog command: ${e.message}`) - ) + }).catch((e: Error) => { + setSpecificErrorDetails({ + message: `Error issuing jog command: ${e.message}`, + }) + }) } } - const { - confirm: confirmExit, - showConfirmation: showConfirmExit, - cancel: cancelExit, - } = useConditionalConfirm(handleCleanUpAndClose, true) - const moveToAddressableArea = ( addressableArea: AddressableAreaName ): Promise => { @@ -326,189 +356,228 @@ export const DropTipWizardComponent = ( ).then(commandData => { const error = commandData[0].data.error if (error != null) { - setErrorMessage(`error moving to position: ${error.detail}`) + setSpecificErrorDetails({ + runCommandError: error, + message: `Error moving to position: ${error.detail}`, + }) } return null }) } else { - setErrorMessage(`error moving to position: invalid addressable area.`) + setSpecificErrorDetails({ + message: `Error moving to position: invalid addressable area.`, + }) return Promise.resolve(null) } } - let modalContent: JSX.Element =
UNASSIGNED STEP
- if (showConfirmExit) { - modalContent = ( - { - hasInitiatedExit.current = true - confirmExit() - }} - isRobotMoving={isRobotMoving} - /> - ) - } else if (errorMessage != null) { - modalContent = ( - - {t('drop_tip_failed')} - {errorMessage} - - } - /> - ) - } else if (shouldDispenseLiquid == null) { - modalContent = ( - - ) - } else if ( - currentStep === CHOOSE_BLOWOUT_LOCATION || - currentStep === CHOOSE_DROP_TIP_LOCATION - ) { - let bodyTextKey - if (currentStep === CHOOSE_BLOWOUT_LOCATION) { - bodyTextKey = isOnDevice - ? 'select_blowout_slot_odd' - : 'select_blowout_slot' + const modalContent = buildModalContent() + + function buildModalContent(): JSX.Element { + if (isRobotMoving) { + return buildRobotInMotion() + } else if (showConfirmExit) { + return buildShowExitConfirmation() + } else if (errorDetails != null) { + return buildErrorScreen() + } else if (shouldDispenseLiquid == null) { + return buildBeforeBeginning() + } else if ( + currentStep === CHOOSE_BLOWOUT_LOCATION || + currentStep === CHOOSE_DROP_TIP_LOCATION + ) { + return buildChooseLocation() + } else if ( + currentStep === POSITION_AND_BLOWOUT || + currentStep === POSITION_AND_DROP_TIP + ) { + return buildJogToPosition() + } else if ( + currentStep === BLOWOUT_SUCCESS || + currentStep === DROP_TIP_SUCCESS + ) { + return buildSuccess() } else { - bodyTextKey = isOnDevice - ? 'select_drop_tip_slot_odd' - : 'select_drop_tip_slot' + return
UNASSIGNED STEP
} - modalContent = ( - { - setCurrentStepIndex(0) - setShouldDispenseLiquid(null) - }} - title={ - currentStep === CHOOSE_BLOWOUT_LOCATION - ? i18n.format(t('choose_blowout_location'), 'capitalize') - : i18n.format(t('choose_drop_tip_location'), 'capitalize') - } - body={ - }} - /> - } - moveToAddressableArea={moveToAddressableArea} - isRobotMoving={isRobotMoving} - isOnDevice={isOnDevice} - setErrorMessage={setErrorMessage} - /> - ) - } else if ( - currentStep === POSITION_AND_BLOWOUT || - currentStep === POSITION_AND_DROP_TIP - ) { - modalContent = ( - { - if (createdMaintenanceRunId != null) { - chainRunCommands( - createdMaintenanceRunId, - [ - currentStep === POSITION_AND_BLOWOUT - ? { - commandType: 'blowOutInPlace', - params: { - pipetteId: MANAGED_PIPETTE_ID, - flowRate: - instrumentModelSpecs.defaultBlowOutFlowRate.value, + + function buildRobotInMotion(): JSX.Element { + return + } + + function buildShowExitConfirmation(): JSX.Element { + return ( + { + hasInitiatedExit.current = true + confirmExit() + }} + /> + ) + } + + function buildErrorScreen(): JSX.Element { + return ( + + {errorExitBtn} + + ) + } + + function buildBeforeBeginning(): JSX.Element { + return ( + + ) + } + + function buildChooseLocation(): JSX.Element { + let bodyTextKey: string + if (currentStep === CHOOSE_BLOWOUT_LOCATION) { + bodyTextKey = isOnDevice + ? 'select_blowout_slot_odd' + : 'select_blowout_slot' + } else { + bodyTextKey = isOnDevice + ? 'select_drop_tip_slot_odd' + : 'select_drop_tip_slot' + } + return ( + { + setCurrentStepIndex(0) + setShouldDispenseLiquid(null) + }} + title={ + currentStep === CHOOSE_BLOWOUT_LOCATION + ? i18n.format(t('choose_blowout_location'), 'capitalize') + : i18n.format(t('choose_drop_tip_location'), 'capitalize') + } + body={ + }} + /> + } + moveToAddressableArea={moveToAddressableArea} + isOnDevice={isOnDevice} + setErrorDetails={setSpecificErrorDetails} + /> + ) + } + + function buildJogToPosition(): JSX.Element { + return ( + { + if (createdMaintenanceRunId != null) { + chainRunCommands( + createdMaintenanceRunId, + [ + currentStep === POSITION_AND_BLOWOUT + ? { + commandType: 'blowOutInPlace', + params: { + pipetteId: MANAGED_PIPETTE_ID, + flowRate: + instrumentModelSpecs.defaultBlowOutFlowRate.value, + }, + } + : { + commandType: 'dropTipInPlace', + params: { pipetteId: MANAGED_PIPETTE_ID }, }, - } - : { - commandType: 'dropTipInPlace', - params: { pipetteId: MANAGED_PIPETTE_ID }, - }, - ], - true - ) - .then(commandData => { - const error = commandData[0].data.error - if (error != null) { - setErrorMessage(`error moving to position: ${error.detail}`) - } else proceed() - }) - .catch(e => - setErrorMessage( - `Error issuing ${ - currentStep === POSITION_AND_BLOWOUT - ? 'blowout' - : 'drop tip' - } command: ${e.message}` - ) + ], + true ) + .then(commandData => { + const error = commandData[0].data.error + if (error != null) { + setSpecificErrorDetails({ + runCommandError: error, + message: `Error moving to position: ${error.detail}`, + }) + } else { + proceed() + } + }) + .catch(e => + setSpecificErrorDetails({ + message: `Error issuing ${ + currentStep === POSITION_AND_BLOWOUT + ? 'blowout' + : 'drop tip' + } command: ${e.message}`, + }) + ) + } + }} + handleGoBack={goBack} + body={ + currentStep === POSITION_AND_BLOWOUT + ? t('position_and_blowout') + : t('position_and_drop_tip') } - }} - isRobotMoving={isRobotMoving} - handleGoBack={goBack} - body={ - currentStep === POSITION_AND_BLOWOUT - ? t('position_and_blowout') - : t('position_and_drop_tip') - } - currentStep={currentStep} - isOnDevice={isOnDevice} - /> - ) - } else if ( - currentStep === BLOWOUT_SUCCESS || - currentStep === DROP_TIP_SUCCESS - ) { - modalContent = ( - - ) + currentStep={currentStep as DropTipWizardStep} + isOnDevice={isOnDevice} + /> + ) + } + + function buildSuccess(): JSX.Element { + return ( + + ) + } } - const hasInitiatedExit = React.useRef(false) - let handleExit: () => void = () => null - if (!hasInitiatedExit.current) handleExit = confirmExit - else if (errorMessage != null) handleExit = handleCleanUpAndClose + const wizardHeaderOnExit = useWizardExitHeader({ + isFinalStep, + hasInitiatedExit: hasInitiatedExit.current, + errorDetails, + confirmExit, + handleCleanUpAndClose, + }) const wizardHeader = ( ) diff --git a/app/src/organisms/DropTipWizard/utils.tsx b/app/src/organisms/DropTipWizard/utils.tsx new file mode 100644 index 000000000000..d0a38fc768b7 --- /dev/null +++ b/app/src/organisms/DropTipWizard/utils.tsx @@ -0,0 +1,185 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { AlertPrimaryButton, SPACING } from '@opentrons/components' + +import { DROP_TIP_SPECIAL_ERROR_TYPES } from './constants' +import { SmallButton } from '../../atoms/buttons' + +import type { RunCommandError } from '@opentrons/api-client' +import type { useChainMaintenanceCommands } from '../../resources/runs' + +export interface ErrorDetails { + message: string + header?: string + type?: string +} + +interface HandleDropTipCommandErrorsCbProps { + runCommandError?: RunCommandError + message?: string + header?: string + type?: RunCommandError['errorType'] +} + +/** + * @description Wraps the error state setter, updating the setter if the error should be special-cased. + */ +export function useHandleDropTipCommandErrors( + setErrorDetails: (errorDetails: ErrorDetails) => void +): (cbProps: HandleDropTipCommandErrorsCbProps) => void { + const { t } = useTranslation('drop_tip_wizard') + + return ({ + runCommandError, + message, + header, + type, + }: HandleDropTipCommandErrorsCbProps) => { + if ( + runCommandError?.errorType === + DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + ) { + const headerText = t('cant_safely_drop_tips') + const messageText = t('remove_the_tips_manually') + + setErrorDetails({ + header: headerText, + message: messageText, + type: DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR, + }) + } else { + const messageText = message ?? '' + setErrorDetails({ header, message: messageText, type }) + } + } +} + +interface DropTipErrorComponents { + button: JSX.Element | null + subHeader: JSX.Element +} + +export interface UseDropTipErrorComponentsProps { + isOnDevice: boolean + t: (translationString: string) => string + maintenanceRunId: string | null + onClose: () => void + errorDetails: ErrorDetails | null + chainRunCommands: ReturnType< + typeof useChainMaintenanceCommands + >['chainRunCommands'] +} + +/** + * @description Returns special-cased components given error details. + */ +export function useDropTipErrorComponents({ + t, + maintenanceRunId, + onClose, + errorDetails, + isOnDevice, + chainRunCommands, +}: UseDropTipErrorComponentsProps): DropTipErrorComponents { + return errorDetails?.type === DROP_TIP_SPECIAL_ERROR_TYPES.MUST_HOME_ERROR + ? buildHandleMustHome() + : buildGenericError() + + function buildGenericError(): DropTipErrorComponents { + return { + button: null, + subHeader: ( + <> + {t('drop_tip_failed')} +
+ {errorDetails?.message} + + ), + } + } + + function buildHandleMustHome(): DropTipErrorComponents { + const handleOnClick = (): void => { + if (maintenanceRunId !== null) { + void chainRunCommands( + maintenanceRunId, + [ + { + commandType: 'home' as const, + params: {}, + }, + ], + true + ) + onClose() + } + } + + return { + button: isOnDevice ? ( + + ) : ( + + {t('confirm_removal_and_home')} + + ), + subHeader: <>{errorDetails?.message}, + } + } +} + +export interface UseWizardExitHeaderProps { + isFinalStep: boolean + hasInitiatedExit: boolean + errorDetails: ErrorDetails | null + handleCleanUpAndClose: (homeOnError?: boolean) => void + confirmExit: (homeOnError?: boolean) => void +} + +/** + * @description Determines the appropriate onClick for the wizard exit button, ensuring the exit logic can occur at + * most one time. + */ +export function useWizardExitHeader({ + isFinalStep, + hasInitiatedExit, + errorDetails, + handleCleanUpAndClose, + confirmExit, +}: UseWizardExitHeaderProps): () => void { + return buildHandleExit() + + function buildHandleExit(): () => void { + if (!hasInitiatedExit) { + if (errorDetails != null) { + // When an error occurs, do not home when exiting the flow via the wizard header. + return buildNoHomeCleanUpAndClose() + } else if (isFinalStep) { + return buildHandleCleanUpAndClose() + } else { + return buildConfirmExit() + } + } else { + return buildGenericCase() + } + } + + function buildGenericCase(): () => void { + return () => null + } + function buildNoHomeCleanUpAndClose(): () => void { + return () => handleCleanUpAndClose(false) + } + function buildHandleCleanUpAndClose(): () => void { + return handleCleanUpAndClose + } + function buildConfirmExit(): () => void { + return confirmExit + } +} diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index cb32ae550b9e..3cd06ff2fd8f 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -73,7 +73,7 @@ function TouchscreenModal({ isEngaged, closeModal, }: EstopPressedModalProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const modalHeader: ModalHeaderBaseProps = { @@ -94,7 +94,7 @@ function TouchscreenModal({ - {t('estop_pressed_description')} + {t('branded:estop_pressed_description')} - {t('estop_pressed_description')} + {t('branded:estop_pressed_description')} - {t('download_logs')} + {t('branded:firmware_update_download_logs')} { subsystem: 'pipette_right', } }) - it('renders text', () => { + it('renders pipette text', () => { const { getByText } = render(props) getByText('Updating pipette firmware...') }) + it('renders Hepa/UV text', () => { + props = { + subsystem: 'hepa_uv', + } + const { getByText } = render(props) + getByText('Updating HEPA/UV Module firmware...') + }) }) diff --git a/app/src/organisms/GripperCard/AboutGripperSlideout.tsx b/app/src/organisms/GripperCard/AboutGripperSlideout.tsx index 9bd6f8f67064..8eea030d6155 100644 --- a/app/src/organisms/GripperCard/AboutGripperSlideout.tsx +++ b/app/src/organisms/GripperCard/AboutGripperSlideout.tsx @@ -22,11 +22,11 @@ export const AboutGripperSlideout = ( props: AboutGripperSlideoutProps ): JSX.Element | null => { const { serialNumber, firmwareVersion, isExpanded, onCloseClick } = props - const { i18n, t } = useTranslation(['device_details', 'shared']) + const { i18n, t } = useTranslation(['device_details', 'shared', 'branded']) return ( { if (createdMaintenanceRunId == null) { createMaintenanceRun({}) @@ -108,7 +108,7 @@ export const BeforeBeginning = ( displayName: t('hex_screwdriver'), subtitle: t('provided_with_robot_use_right_size'), }, - [GRIPPER_LOADNAME]: { displayName: t('gripper') }, + [GRIPPER_LOADNAME]: { displayName: t('branded:gripper') }, } const { bodyI18nKey, equipmentLoadNames } = INFO_BY_FLOW_TYPE[flowType] diff --git a/app/src/organisms/GripperWizardFlows/MountGripper.tsx b/app/src/organisms/GripperWizardFlows/MountGripper.tsx index 7e1636f6d058..a7049cf447d4 100644 --- a/app/src/organisms/GripperWizardFlows/MountGripper.tsx +++ b/app/src/organisms/GripperWizardFlows/MountGripper.tsx @@ -58,7 +58,7 @@ export const MountGripper = ( props: GripperWizardStepProps ): JSX.Element | null => { const { proceed, isRobotMoving } = props - const { t } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t } = useTranslation(['gripper_wizard_flows', 'shared', 'branded']) const isOnDevice = useSelector(getIsOnDevice) const [showUnableToDetect, setShowUnableToDetect] = React.useState(false) const [isPending, setIsPending] = React.useState(false) @@ -119,7 +119,7 @@ export const MountGripper = (
) : ( { const { proceed, successfulAction, isRobotMoving } = props - const { t, i18n } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'gripper_wizard_flows', + 'shared', + 'branded', + ]) const isOnDevice = useSelector(getIsOnDevice) const infoByAction: { @@ -46,11 +50,11 @@ export const Success = ( } } = { [SUCCESSFULLY_ATTACHED_AND_CALIBRATED]: { - header: t('gripper_successfully_attached_and_calibrated'), + header: t('branded:gripper_successfully_attached_and_calibrated'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, [SUCCESSFULLY_CALIBRATED]: { - header: t('gripper_successfully_calibrated'), + header: t('branded:gripper_successfully_calibrated'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, [SUCCESSFULLY_ATTACHED]: { @@ -58,7 +62,7 @@ export const Success = ( buttonText: t('calibrate_gripper'), }, [SUCCESSFULLY_DETACHED]: { - header: t('gripper_successfully_detached'), + header: t('branded:gripper_successfully_detached'), buttonText: i18n.format(t('shared:exit'), 'capitalize'), }, } diff --git a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx index c8e25bc02285..f0b2467e95d2 100644 --- a/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx +++ b/app/src/organisms/GripperWizardFlows/UnmountGripper.tsx @@ -51,7 +51,7 @@ export const UnmountGripper = ( props: GripperWizardStepProps ): JSX.Element | null => { const { proceed, isRobotMoving, goBack, chainRunCommands } = props - const { t } = useTranslation(['gripper_wizard_flows', 'shared']) + const { t } = useTranslation(['gripper_wizard_flows', 'shared', 'branded']) const isOnDevice = useSelector(getIsOnDevice) const [isPending, setIsPending] = React.useState(false) const { data: instrumentsQueryData, refetch } = useInstrumentsQuery({ @@ -100,7 +100,7 @@ export const UnmountGripper = ( return showGripperStillDetected ? ( ) : ( { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) const labwareInfo = res[0] expect(labwareInfo).toBeTruthy() @@ -154,7 +154,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res).toHaveLength(1) // the offdeck labware still gets added because the mating surface doesn't exist for offdeck labware }) @@ -163,7 +163,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res).toHaveLength(2) const labwareInfo = res.find( @@ -172,7 +172,7 @@ describe('getRunLabwareRenderInfo', () => { expect(labwareInfo).toBeTruthy() expect(labwareInfo?.x).toEqual(0) expect(labwareInfo?.y).toEqual( - ot2DeckDefV4.cornerOffsetFromOrigin[1] - + ot2DeckDefV5.cornerOffsetFromOrigin[1] - mockLabwareDefinition.dimensions.yDimension ) }) @@ -189,7 +189,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( { labware: [mockBadSlotLabware] } as any, mockLabwareDefinitionsByUri, - ot2DeckDefV4 as any + ot2DeckDefV5 as any ) expect(res[0].x).toEqual(0) @@ -207,7 +207,7 @@ describe('getCurrentRunModuleRenderInfo', () => { it('returns run module render info with nested labware', () => { const res = getRunModuleRenderInfo( mockRunData, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) const moduleInfo = res[0] @@ -228,7 +228,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataNoNesting, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) @@ -245,7 +245,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithTC, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) @@ -270,7 +270,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithBadModuleSlot, - ot2DeckDefV4 as any, + ot2DeckDefV5 as any, mockLabwareDefinitionsByUri ) diff --git a/app/src/organisms/LabwareCard/index.tsx b/app/src/organisms/LabwareCard/index.tsx index 708639711b24..2413b10e4fcd 100644 --- a/app/src/organisms/LabwareCard/index.tsx +++ b/app/src/organisms/LabwareCard/index.tsx @@ -30,7 +30,7 @@ export interface LabwareCardProps { } export function LabwareCard(props: LabwareCardProps): JSX.Element { - const { t } = useTranslation('labware_landing') + const { t } = useTranslation(['labware_landing', 'branded']) const { definition, modified, filename } = props.labware const apiName = definition.parameters.loadName const displayName = definition?.metadata.displayName @@ -100,7 +100,7 @@ export function LabwareCard(props: LabwareCardProps): JSX.Element { id="LabwareCard_opentronsDef" marginLeft={SPACING.spacing4} > - {t('opentrons_def')} + {t('branded:opentrons_def')} )} diff --git a/app/src/organisms/LabwareDetails/index.tsx b/app/src/organisms/LabwareDetails/index.tsx index 4f0cc83b3a48..7787e13f57f6 100644 --- a/app/src/organisms/LabwareDetails/index.tsx +++ b/app/src/organisms/LabwareDetails/index.tsx @@ -65,7 +65,7 @@ export interface LabwareDetailsProps { } export function LabwareDetails(props: LabwareDetailsProps): JSX.Element { - const { t } = useTranslation('labware_landing') + const { t } = useTranslation(['labware_landing', 'branded']) const { definition, modified, filename } = props.labware const { metadata, parameters, brand, wells, ordering } = definition const apiName = definition.parameters.loadName @@ -129,7 +129,7 @@ export function LabwareDetails(props: LabwareDetailsProps): JSX.Element { id="LabwareDetails_opentronsDef" marginLeft={SPACING.spacing4} > - {t('opentrons_def')} + {t('branded:opentrons_def')}
)} diff --git a/app/src/organisms/ModuleCard/ErrorInfo.tsx b/app/src/organisms/ModuleCard/ErrorInfo.tsx index 75158e7010f6..d8bb5e28b6e1 100644 --- a/app/src/organisms/ModuleCard/ErrorInfo.tsx +++ b/app/src/organisms/ModuleCard/ErrorInfo.tsx @@ -29,7 +29,7 @@ interface ErrorInfoProps { } export function ErrorInfo(props: ErrorInfoProps): JSX.Element | null { const { attachedModule } = props - const { t } = useTranslation(['device_details', 'shared']) + const { t } = useTranslation(['device_details', 'shared', 'branded']) const [showErrorDetails, setShowErrorDetails] = React.useState(false) let isError: boolean = false @@ -92,7 +92,7 @@ export function ErrorInfo(props: ErrorInfoProps): JSX.Element | null { {errorDetails} ) : null} - {t('module_error_contact_support')} + {t('branded:module_error_contact_support')}
diff --git a/app/src/organisms/ModuleCard/ModuleSetupModal.tsx b/app/src/organisms/ModuleCard/ModuleSetupModal.tsx index 8af56a5bcf4a..21e3adb598a8 100644 --- a/app/src/organisms/ModuleCard/ModuleSetupModal.tsx +++ b/app/src/organisms/ModuleCard/ModuleSetupModal.tsx @@ -26,7 +26,7 @@ interface ModuleSetupModalProps { export const ModuleSetupModal = (props: ModuleSetupModalProps): JSX.Element => { const { moduleDisplayName } = props - const { t, i18n } = useTranslation(['protocol_setup', 'shared']) + const { t, i18n } = useTranslation(['protocol_setup', 'shared', 'branded']) return createPortal( { width="50%" > - {t('modal_instructions')} + {t('branded:modal_instructions')} { attachedModule, attachedPipette, isOnDevice, - slotName, + deckConfig, + fixtureIdByCutoutId, } = props const { t, i18n } = useTranslation([ 'module_wizard_flows', @@ -65,12 +73,11 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { probeLocation = t('pipette_wizard_flows:ninety_six_probe_location') break } - const wasteChuteConflict = - slotName === 'C3' && attachedPipette.data.channels === 96 - const deckConfig = useDeckConfigurationQuery().data - const isWasteChuteOnDeck = - deckConfig?.find(fixture => fixture.cutoutId === WASTE_CHUTE_CUTOUT) ?? - false + const wasteChuteConflictWith96Channel = + 'cutoutC3' in fixtureIdByCutoutId && attachedPipette.data.channels === 96 + const isWasteChuteOnDeck = deckConfig.some(cc => + WASTE_CHUTE_FIXTURES.includes(cc.cutoutFixtureId) + ) const pipetteAttachProbeVid = ( @@ -101,7 +108,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { /> - {wasteChuteConflict && ( + {wasteChuteConflictWith96Channel && ( , - CreateMaintenanceRunData, - unknown - > - isCreateLoading: boolean - createdMaintenanceRunId: string | null -} +type BeforeBeginningProps = ModuleCalibrationWizardStepProps export const BeforeBeginning = ( props: BeforeBeginningProps ): JSX.Element | null => { - const { - proceed, - createMaintenanceRun, - isCreateLoading, - attachedModule, - maintenanceRunId, - createdMaintenanceRunId, - } = props + const { proceed, attachedModule } = props const { t } = useTranslation(['module_wizard_flows', 'shared']) - React.useEffect(() => { - if (createdMaintenanceRunId == null) { - createMaintenanceRun({}) - } - }, []) + const moduleDisplayName = getModuleDisplayName(attachedModule.moduleModel) let adapterLoadname: string @@ -103,13 +77,12 @@ export const BeforeBeginning = ( bodyText={ }} /> } proceedButtonText={t('start_setup')} - proceedIsDisabled={isCreateLoading || maintenanceRunId == null} proceed={proceed} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx index cd5c949d8ba6..b5d5e5cf80d3 100644 --- a/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx +++ b/app/src/organisms/ModuleWizardFlows/PlaceAdapter.tsx @@ -16,7 +16,6 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { - CreateCommand, getCalibrationAdapterLoadName, getModuleDisplayName, HEATERSHAKER_MODULE_TYPE, @@ -24,17 +23,34 @@ import { HEATERSHAKER_MODULE_MODELS, TEMPERATURE_MODULE_MODELS, THERMOCYCLER_MODULE_MODELS, + FLEX_SINGLE_SLOT_BY_CUTOUT_ID, + THERMOCYCLER_V2_FRONT_FIXTURE, } from '@opentrons/shared-data' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import { LEFT_SLOTS } from './constants' +import type { DeckConfiguration, CreateCommand } from '@opentrons/shared-data' import type { ModuleCalibrationWizardStepProps } from './types' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' +import type { AxiosError } from 'axios' +import type { UseMutateFunction } from 'react-query' +import type { + CreateMaintenanceRunData, + MaintenanceRun, +} from '@opentrons/api-client' interface PlaceAdapterProps extends ModuleCalibrationWizardStepProps { - slotName: string + deckConfig: DeckConfiguration setCreatedAdapterId: (adapterId: string) => void + createMaintenanceRun: UseMutateFunction< + MaintenanceRun, + AxiosError, + CreateMaintenanceRunData, + unknown + > + isCreateLoading: boolean + createdMaintenanceRunId: string | null } export const BODY_STYLE = css` @@ -50,16 +66,33 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { const { proceed, goBack, + deckConfig, attachedModule, - slotName, chainRunCommands, setErrorMessage, setCreatedAdapterId, attachedPipette, isRobotMoving, + maintenanceRunId, + createMaintenanceRun, + isCreateLoading, + createdMaintenanceRunId, } = props const { t } = useTranslation('module_wizard_flows') + React.useEffect(() => { + if (createdMaintenanceRunId == null) { + createMaintenanceRun({}) + } + }, []) const mount = attachedPipette.mount + const cutoutId = deckConfig.find( + cc => + cc.opentronsModuleSerialNumber === attachedModule.serialNumber && + (attachedModule.moduleType !== THERMOCYCLER_MODULE_TYPE || + cc.cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE) + )?.cutoutId + const slotName = + cutoutId != null ? FLEX_SINGLE_SLOT_BY_CUTOUT_ID[cutoutId] : null const handleOnClick = (): void => { const calibrationAdapterLoadName = getCalibrationAdapterLoadName( attachedModule.moduleModel @@ -70,15 +103,19 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { ) return } + if (slotName == null) { + console.error( + `could not load module ${attachedModule.moduleModel} into location ${slotName}` + ) + return + } const calibrationAdapterId = uuidv4() const commands: CreateCommand[] = [ { commandType: 'loadModule', params: { - location: { - slotName: slotName, - }, + location: { slotName }, model: attachedModule.moduleModel, moduleId: attachedModule.id, }, @@ -97,15 +134,21 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { { commandType: 'calibration/moveToMaintenancePosition', params: { - mount: mount, + mount, maintenancePosition: 'attachInstrument', }, }, ] chainRunCommands?.(commands, false) - .then(() => setCreatedAdapterId(calibrationAdapterId)) - .then(() => proceed()) - .catch((e: Error) => setErrorMessage(e.message)) + .then(() => { + setCreatedAdapterId(calibrationAdapterId) + }) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setErrorMessage(e.message) + }) } const moduleType = attachedModule.moduleType @@ -188,6 +231,7 @@ export const PlaceAdapter = (props: PlaceAdapterProps): JSX.Element | null => { bodyText={bodyText} proceedButtonText={t('confirm_placement')} proceed={handleOnClick} + proceedIsDisabled={isCreateLoading || maintenanceRunId == null} back={goBack} /> ) diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index 6a6949590798..af0301549d0a 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -1,15 +1,23 @@ import * as React from 'react' +import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { - FLEX_ROBOT_TYPE, - getDeckDefFromRobotType, getModuleDisplayName, - THERMOCYCLER_MODULE_TYPE, - CutoutConfig, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, + getCutoutFixturesForModuleModel, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_CENTER_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_CUTOUTS, + SINGLE_RIGHT_SLOT_FIXTURE, + getFixtureIdByCutoutIdFromModuleAnchorCutoutId, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { - DeckLocationSelect, + DeckConfigurator, RESPONSIVENESS, SIZE_1, SPACING, @@ -19,6 +27,12 @@ import { import { Banner } from '../../atoms/Banner' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { ModuleCalibrationWizardStepProps } from './types' +import type { + CutoutConfig, + DeckConfiguration, + CutoutFixtureId, + CutoutId, +} from '@opentrons/shared-data' export const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @@ -29,9 +43,10 @@ export const BODY_STYLE = css` } ` interface SelectLocationProps extends ModuleCalibrationWizardStepProps { - setSlotName: React.Dispatch> availableSlotNames: string[] occupiedCutouts: CutoutConfig[] + deckConfig: DeckConfiguration + configuredFixtureIdByCutoutId: { [cutoutId in CutoutId]?: CutoutFixtureId } } export const SelectLocation = ( props: SelectLocationProps @@ -39,17 +54,19 @@ export const SelectLocation = ( const { proceed, attachedModule, - slotName, - setSlotName, - availableSlotNames, - occupiedCutouts, + deckConfig, + configuredFixtureIdByCutoutId, } = props const { t } = useTranslation('module_wizard_flows') const moduleName = getModuleDisplayName(attachedModule.moduleModel) const handleOnClick = (): void => { proceed() } + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const cutoutConfig = deckConfig.find( + cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + ) const bodyText = ( <> @@ -61,28 +78,111 @@ export const SelectLocation = ( ) + const moduleFixtures = getCutoutFixturesForModuleModel( + attachedModule.moduleModel, + deckDef + ) + const mayMountToCutoutIds = moduleFixtures.reduce( + (acc, { mayMountTo }) => [...acc, ...mayMountTo], + [] + ) + const editableCutoutIds = deckConfig.reduce( + (acc, { cutoutId, cutoutFixtureId, opentronsModuleSerialNumber }) => { + const isCurrentConfiguration = + Object.values(configuredFixtureIdByCutoutId).includes( + cutoutFixtureId + ) && attachedModule.serialNumber === opentronsModuleSerialNumber + if ( + mayMountToCutoutIds.includes(cutoutId) && + (isCurrentConfiguration || + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId)) + ) { + return [...acc, cutoutId] + } + return acc + }, + [] + ) + + const handleAddFixture = (anchorCutoutId: CutoutId): void => { + const selectedFixtureIdByCutoutIds = getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures + ) + if (!isEqual(selectedFixtureIdByCutoutIds, configuredFixtureIdByCutoutId)) { + updateDeckConfiguration( + deckConfig.map(cc => { + if (cc.cutoutId in configuredFixtureIdByCutoutId) { + let replacementFixtureId: CutoutFixtureId = SINGLE_LEFT_SLOT_FIXTURE + if (SINGLE_CENTER_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_CENTER_SLOT_FIXTURE + } else if (SINGLE_RIGHT_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } + return { + ...cc, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + } else if (cc.cutoutId in selectedFixtureIdByCutoutIds) { + return { + ...cc, + cutoutFixtureId: + selectedFixtureIdByCutoutIds[cc.cutoutId] ?? cc.cutoutFixtureId, + opentronsModuleSerialNumber: attachedModule.serialNumber, + } + } else { + return cc + } + }) + ) + } + } + + const handleRemoveFixture = (anchorCutoutId: CutoutId): void => { + const removedFixtureIdByCutoutIds = getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures + ) + updateDeckConfiguration( + deckConfig.map(cc => { + if (cc.cutoutId in removedFixtureIdByCutoutIds) { + let replacementFixtureId: CutoutFixtureId = SINGLE_LEFT_SLOT_FIXTURE + if (SINGLE_CENTER_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_CENTER_SLOT_FIXTURE + } else if (SINGLE_RIGHT_CUTOUTS.includes(cc.cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } + return { + ...cc, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + } else { + return cc + } + }) + ) + } + return ( setSlotName(loc.slotName)} - availableSlotNames={availableSlotNames} - occupiedCutouts={occupiedCutouts} - isThermocycler={ - attachedModule.moduleType === THERMOCYCLER_MODULE_TYPE - } - showTooltipOnDisabled={true} + } bodyText={bodyText} proceedButtonText={t('confirm_location')} proceed={handleOnClick} - proceedIsDisabled={slotName == null} + proceedIsDisabled={cutoutConfig == null} disableProceedReason={ - slotName == null + cutoutConfig == null ? 'Current deck configuration prevents module placement' : undefined } diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 39c235bd7828..3e0977a4f23b 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -13,6 +13,10 @@ import { getModuleDisplayName, FLEX_CUTOUT_BY_SLOT_ID, SINGLE_SLOT_FIXTURES, + getFixtureIdByCutoutIdFromModuleSlotName, + getCutoutFixturesForModuleModel, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' @@ -45,7 +49,6 @@ interface ModuleWizardFlowsProps { attachedModule: AttachedModule closeFlow: () => void isPrepCommandLoading: boolean - initialSlotName?: string onComplete?: () => void prepCommandErrorMessage?: string } @@ -57,7 +60,6 @@ export const ModuleWizardFlows = ( ): JSX.Element | null => { const { attachedModule, - initialSlotName, isPrepCommandLoading, closeFlow, onComplete, @@ -72,12 +74,25 @@ export const ModuleWizardFlows = ( : attachedPipettes.right const moduleCalibrationSteps = getModuleCalibrationSteps() + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const deckConfig = useDeckConfigurationQuery().data ?? [] + const moduleCutoutConfig = deckConfig.find( + cc => cc.opentronsModuleSerialNumber === attachedModule.serialNumber + ) + // mapping of cutoutId's occupied by the target module and their cutoutFixtureId's per cutout + const fixtureIdByCutoutId = + moduleCutoutConfig != null + ? getFixtureIdByCutoutIdFromModuleSlotName( + moduleCutoutConfig.cutoutId.replace('cutout', ''), + getCutoutFixturesForModuleModel(attachedModule.moduleModel, deckDef), + deckDef + ) + : {} const occupiedCutouts = deckConfig.filter( - (fixture: CutoutConfig) => + (cutoutConfig: CutoutConfig) => !SINGLE_SLOT_FIXTURES.includes( - fixture.cutoutFixtureId as SingleSlotCutoutFixtureId - ) + cutoutConfig.cutoutFixtureId as SingleSlotCutoutFixtureId + ) && !Object.keys(fixtureIdByCutoutId).includes(cutoutConfig.cutoutId) ) const availableSlotNames = FLEX_SLOT_NAMES_BY_MOD_TYPE[ @@ -90,9 +105,6 @@ export const ModuleWizardFlows = ( ) ) ?? [] - const [slotName, setSlotName] = React.useState( - initialSlotName != null ? initialSlotName : availableSlotNames?.[0] ?? null - ) const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 const currentStep = moduleCalibrationSteps?.[currentStepIndex] @@ -247,7 +259,6 @@ export const ModuleWizardFlows = ( errorMessage, isOnDevice, attachedModule, - slotName, isExiting, } @@ -276,7 +287,7 @@ export const ModuleWizardFlows = ( ) : ( , @@ -289,23 +300,16 @@ export const ModuleWizardFlows = ( } else if (isExiting) { modalContent = } else if (currentStep.section === SECTIONS.BEFORE_BEGINNING) { - modalContent = ( - - ) + modalContent = } else if (currentStep.section === SECTIONS.SELECT_LOCATION) { modalContent = ( ) } else if (currentStep.section === SECTIONS.PLACE_ADAPTER) { @@ -313,7 +317,11 @@ export const ModuleWizardFlows = ( ) } else if (currentStep.section === SECTIONS.ATTACH_PROBE) { @@ -321,7 +329,9 @@ export const ModuleWizardFlows = ( ) } else if (currentStep.section === SECTIONS.DETACH_PROBE) { diff --git a/app/src/organisms/ModuleWizardFlows/types.ts b/app/src/organisms/ModuleWizardFlows/types.ts index f2b2764b12c3..df6020e9b362 100644 --- a/app/src/organisms/ModuleWizardFlows/types.ts +++ b/app/src/organisms/ModuleWizardFlows/types.ts @@ -24,7 +24,6 @@ export interface ModuleCalibrationWizardStepProps { attachedPipette: PipetteInformation errorMessage: string | null setErrorMessage: (message: string | null) => void - slotName: string isOnDevice: boolean | null } diff --git a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx index b0e365d08fce..375476f2a2ef 100644 --- a/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx +++ b/app/src/organisms/NetworkSettings/AlternativeSecurityTypeModal.tsx @@ -26,7 +26,7 @@ interface AlternativeSecurityTypeModalProps { export function AlternativeSecurityTypeModal({ setShowAlternativeSecurityTypeModal, }: AlternativeSecurityTypeModalProps): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const history = useHistory() const modalHeader: ModalHeaderBaseProps = { title: t('alternative_security_types'), @@ -58,7 +58,7 @@ export function AlternativeSecurityTypeModal({ fontWeight={TYPOGRAPHY.fontWeightRegular} color={COLORS.grey60} > - {t('alternative_security_types_description')} + {t('branded:alternative_security_types_description')} 0 + : false + const handleCardClick = (): void => { setShowSpinner(true) - cloneRun() - trackEvent({ - name: 'proceedToRun', - properties: { sourceLocation: 'RecentRunProtocolCard' }, - }) + if (hasRunTimeParameters) { + history.push(`/protocols/${protocolId}`) + } else { + cloneRun() + trackEvent({ + name: 'proceedToRun', + properties: { sourceLocation: 'RecentRunProtocolCard' }, + }) + } // TODO(BC, 08/29/23): reintroduce this analytics event when we refactor the hook to fetch data lazily (performance concern) // trackProtocolRunEvent({ name: 'runAgain' }) } diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index 1584e3ce723c..10de409948a6 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -5,8 +5,14 @@ import { MemoryRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { when } from 'vitest-when' -import { useProtocolQuery } from '@opentrons/react-api-client' -import { RUN_STATUS_FAILED } from '@opentrons/api-client' +import { + useProtocolQuery, + useProtocolAnalysisAsDocumentQuery, +} from '@opentrons/react-api-client' +import { + RUN_STATUS_FAILED, + simpleAnalysisFileFixture, +} from '@opentrons/api-client' import { COLORS } from '@opentrons/components' import { renderWithProviders } from '../../../../__testing-utils__' @@ -24,11 +30,23 @@ import { INIT_STATUS, } from '../../../../resources/health/hooks' +import type { useHistory } from 'react-router-dom' import type { ProtocolHardware } from '../../../../pages/Protocols/hooks' +const mockPush = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useHistory: () => ({ push: mockPush } as any), + } +}) + vi.mock('@opentrons/react-api-client') vi.mock('../../../../atoms/Skeleton') vi.mock('../../../../pages/Protocols/hooks') +vi.mock('../../../../pages/ProtocolDetails') vi.mock('../../../../organisms/Devices/hooks') vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock('../../../../organisms/ProtocolUpload/hooks') @@ -128,7 +146,18 @@ describe('RecentRunProtocolCard', () => { data: { data: [mockRunData] }, } as any) vi.mocked(useProtocolQuery).mockReturnValue({ - data: { data: { metadata: { protocolName: 'mockProtocol' } } }, + data: { + data: { + metadata: { protocolName: 'mockProtocol' }, + id: 'mockProtocolId', + }, + }, + } as any) + vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ + data: { + ...simpleAnalysisFileFixture, + runTimeParameters: [], + }, } as any) vi.mocked(useRobotInitializationStatus).mockReturnValue( INIT_STATUS.SUCCEEDED @@ -252,4 +281,14 @@ describe('RecentRunProtocolCard', () => { const [{ getByText }] = render(props) getByText('mock Skeleton') }) + + it('should push to protocol details if protocol contains runtime parameters', () => { + vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ + data: simpleAnalysisFileFixture, + } as any) + render(props) + const button = screen.getByLabelText('RecentRunProtocolCard') + fireEvent.click(button) + expect(mockPush).toBeCalledWith('/protocols/mockProtocolId') + }) }) diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx index c28411011339..b29b81f76aab 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal.tsx @@ -98,7 +98,7 @@ export function ConfirmCancelRunModal({ paddingBottom={SPACING.spacing32} paddingTop={`${isActiveRun ? SPACING.spacing32 : '0'}`} > - {t('cancel_run_alert_info')} + {t('cancel_run_alert_info_flex')} {t('cancel_run_module_info')} - {t('contact_information')} + {t('branded:contact_information')} void + errorType?: string + protocolName?: string +} + +export function RunPausedSplash({ + onClose, + errorType, + protocolName, +}: RunPausedSplashProps): JSX.Element { + const { t } = useTranslation('error_recovery') + + let subText: string | null + switch (errorType) { + default: + subText = protocolName ?? null + } + + return ( + + + + + {t('run_paused')} + + + {subText} + + + + ) +} + +const SplashHeader = styled.h1` + font-weight: ${TYPOGRAPHY.fontWeightBold}; + text-align: ${TYPOGRAPHY.textAlignLeft}; + font-size: ${TYPOGRAPHY.fontSize80}; + line-height: ${TYPOGRAPHY.lineHeight96}; + color: ${COLORS.white}; +` +const SplashBody = styled.h4` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; + overflow-wrap: ${OVERFLOW_WRAP_BREAK_WORD}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + text-align: ${TYPOGRAPHY.textAlignCenter}; + text-transform: ${TYPOGRAPHY.textTransformCapitalize}; + font-size: ${TYPOGRAPHY.fontSize32}; + line-height: ${TYPOGRAPHY.lineHeight42}; + color: ${COLORS.white}; +` + +const SplashFrame = styled(Flex)` + width: 100%; + height: 100%; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing40}; +` diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx index fad8b4b8de1d..36e23a35df6d 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/ConfirmCancelRunModal.test.tsx @@ -97,12 +97,10 @@ describe('ConfirmCancelRunModal', () => { vi.restoreAllMocks() }) - it('should render text and buttons', () => { + it('should render correct text and buttons', () => { render(props) - screen.getByText('Are you sure you want to cancel this run?') - screen.getByText( - 'Doing so will terminate this run, drop any attached tips in the trash container and home your robot.' - ) + screen.getByText('Are you sure you want to cancel?') + screen.getByText('Doing so will terminate this run and home your robot.') screen.getByText( 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' ) @@ -111,7 +109,7 @@ describe('ConfirmCancelRunModal', () => { screen.getByText('Cancel run') }) - it('shoudler render the canceling run modal when run is dismissing', () => { + it('should render the canceling run modal when run is dismissing', () => { vi.mocked(useDismissCurrentRunMutation).mockReturnValue({ dismissCurrentRun: mockDismissCurrentRun, isLoading: true, diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx new file mode 100644 index 000000000000..6a7061346a4a --- /dev/null +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/__tests__/RunPausedSplash.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react' +import { MemoryRouter } from 'react-router-dom' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' + +import { COLORS } from '@opentrons/components' + +import { RunPausedSplash } from '../RunPausedSplash' + +const render = (props: React.ComponentProps) => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + ) +} + +const MOCK_PROTOCOL_NAME = 'MOCK_PROTOCOL' + +describe('ConfirmCancelRunModal', () => { + let props: React.ComponentProps + const mockOnClose = vi.fn() + + beforeEach(() => { + props = { + onClose: mockOnClose, + protocolName: MOCK_PROTOCOL_NAME, + errorType: '', + } + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should render a generic paused screen if there is no errorType', () => { + render(props) + expect(screen.getByText('Run paused')).toBeInTheDocument() + expect(screen.getByText(MOCK_PROTOCOL_NAME)) + expect(screen.getByRole('button')).toHaveStyle({ + 'background-color': COLORS.grey50, + }) + fireEvent.click(screen.getByRole('button')) + expect(mockOnClose).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx index 8d9330315c7b..f3926a711a37 100644 --- a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx @@ -207,7 +207,11 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => {
) : ( - + {showExitConfirmation ? ( setShowExitConfirmation(false)} @@ -218,7 +222,7 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { ) : ( diff --git a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx index 1056cf9831bd..b73111af4201 100644 --- a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx +++ b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx @@ -26,14 +26,20 @@ interface ProbeNotAttachedProps { export const ProbeNotAttached = ( props: ProbeNotAttachedProps ): JSX.Element | null => { - const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'pipette_wizard_flows', + 'shared', + 'branded', + ]) const { isOnDevice, handleOnClick, setShowUnableToDetect } = props const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) return ( 2 ? t('something_seems_wrong') : undefined} + subHeader={ + numberOfTryAgains > 2 ? t('branded:something_seems_wrong') : undefined + } iconColor={COLORS.red50} isSuccess={false} > diff --git a/app/src/organisms/PipetteWizardFlows/Results.tsx b/app/src/organisms/PipetteWizardFlows/Results.tsx index 04549e686dfa..fda578001516 100644 --- a/app/src/organisms/PipetteWizardFlows/Results.tsx +++ b/app/src/organisms/PipetteWizardFlows/Results.tsx @@ -60,7 +60,11 @@ export const Results = (props: ResultsProps): JSX.Element => { setShowErrorMessage, nextMount, } = props - const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) + const { t, i18n } = useTranslation([ + 'pipette_wizard_flows', + 'shared', + 'branded', + ]) const pipetteName = attachedPipettes[mount] != null ? attachedPipettes[mount]?.displayName : '' @@ -263,7 +267,8 @@ export const Results = (props: ResultsProps): JSX.Element => { } } ` - subHeader = numberOfTryAgains > 2 ? t('something_seems_wrong') : undefined + subHeader = + numberOfTryAgains > 2 ? t('branded:something_seems_wrong') : undefined button = ( <> {isOnDevice ? ( diff --git a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx index 5355349b6569..497e5fc19b08 100644 --- a/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx +++ b/app/src/organisms/PipetteWizardFlows/UnskippableModal.tsx @@ -32,7 +32,7 @@ export function UnskippableModal(props: UnskippableModalProps): JSX.Element { diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx index 37570d8c5ffe..bca0e6236191 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx @@ -150,7 +150,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' ) }) @@ -163,7 +163,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' ) }) }) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx index fd28aa5e8dfc..43fa441c7d14 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/UnskippableModal.test.tsx @@ -41,7 +41,9 @@ describe('UnskippableModal', () => { screen.getByText( 'You must detach the mounting plate and reattach the z-axis carraige before using other pipettes. We do not recommend exiting this process before completion.' ) - fireEvent.click(screen.getByRole('button', { name: 'exit' })) + screen.getByText('Exit') + screen.getByText('Go back') + fireEvent.click(screen.getByRole('button', { name: 'Exit' })) expect(props.proceed).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/PipetteWizardFlows/index.tsx b/app/src/organisms/PipetteWizardFlows/index.tsx index 1a671fb31fb6..337a51028ed9 100644 --- a/app/src/organisms/PipetteWizardFlows/index.tsx +++ b/app/src/organisms/PipetteWizardFlows/index.tsx @@ -421,7 +421,7 @@ export const PipetteWizardFlows = ( currentStep.section === SECTIONS.BEFORE_BEGINNING && selectedPipette === NINETY_SIX_CHANNEL && flowType === FLOWS.ATTACH - ? '70%' + ? '30rem' : 'auto' } header={wizardHeader} diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx index 707aa5256cfb..191329bbae87 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -7,6 +7,17 @@ import { i18n } from '../../../../i18n' import { ProtocolParameters } from '..' import type { RunTimeParameter } from '@opentrons/shared-data' +import type * as Components from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + NoParameters: vi.fn(() => ( +
No parameters specified in this protocol
+ )), + } +}) const mockRunTimeParameter: RunTimeParameter[] = [ { @@ -14,7 +25,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -111,11 +122,11 @@ describe('ProtocolParameters', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') screen.getByText('pipette mount') screen.getByText('Left') diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx index d7a64fd2396e..797e18b930dc 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, + InfoScreen, + ParametersTable, SPACING, StyledText, TYPOGRAPHY, - ParametersTable, - NoParameters, } from '@opentrons/components' import { Banner } from '../../../atoms/Banner' @@ -48,7 +48,7 @@ export function ProtocolParameters({
) : ( - + )}
) diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index 9329b6329b38..2dc5ecda65f0 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -73,7 +73,6 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' import { ProtocolParameters } from './ProtocolParameters' -import { useRunTimeParameters } from '../../pages/Protocols/hooks' import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' @@ -200,10 +199,11 @@ export function ProtocolDetails( const { protocolKey, srcFileNames, mostRecentAnalysis, modified } = props const { t, i18n } = useTranslation(['protocol_details', 'shared']) const enableProtocolStats = useFeatureFlag('protocolStats') - const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = runTimeParameters.length > 0 const [currentTab, setCurrentTab] = React.useState< 'robot_config' | 'labware' | 'liquids' | 'stats' | 'parameters' - >('robot_config') + >(hasRunTimeParameters ? 'parameters' : 'robot_config') const [ showChooseRobotToRunProtocolSlideout, setShowChooseRobotToRunProtocolSlideout, @@ -218,8 +218,6 @@ export function ProtocolDetails( getIsProtocolAnalysisInProgress(state, protocolKey) ) - const runTimeParameters = useRunTimeParameters(protocolKey) - const analysisStatus = getAnalysisStatus(isAnalyzing, mostRecentAnalysis) if (analysisStatus === 'stale') { @@ -333,9 +331,7 @@ export function ProtocolDetails( stats: enableProtocolStats ? ( ) : null, - parameters: enableRunTimeParameters ? ( - - ) : null, + parameters: , } const deckMap = @@ -394,7 +390,6 @@ export function ProtocolDetails( onCloseClick={() => setShowChooseRobotToRunProtocolSlideout(false)} showSlideout={showChooseRobotToRunProtocolSlideout} storedProtocolData={props} - runTimeParameters={runTimeParameters} /> - {enableRunTimeParameters && mostRecentAnalysis != null && ( + {mostRecentAnalysis != null && ( { vi.mocked(useDeckConfigurationQuery).mockReturnValue(({ data: [], } as unknown) as UseQueryResult) + vi.mocked(useModulesQuery).mockReturnValue(({ + data: { data: [] }, + } as unknown) as UseQueryResult) }) afterEach(() => { diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index 57d7981c138c..b44a314983ac 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -8,7 +8,7 @@ import { useCreateLiveCommandMutation, useModulesQuery, } from '@opentrons/react-api-client' -import { ot3StandardDeckV4 as ot3StandardDeckDef } from '@opentrons/shared-data' +import { ot3StandardDeckV5 as ot3StandardDeckDef } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index bdb68944a675..3bc1ad62c564 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -108,6 +108,7 @@ export function ProtocolSetupLabware({ mostRecentAnalysis != null ? getProtocolModulesInfo(mostRecentAnalysis, deckDef) : [] + const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, protocolModulesInfo diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index e2dbb107379f..23d490af2872 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -16,6 +16,7 @@ import { } from '@opentrons/components' import { getCutoutDisplayName, + getDeckDefFromRobotType, getFixtureDisplayName, getSimplestDeckConfigForProtocol, SINGLE_SLOT_FIXTURES, @@ -30,6 +31,7 @@ import type { CompletedProtocolAnalysis, CutoutFixtureId, CutoutId, + DeckDefinition, RobotType, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/ProtocolSetup' @@ -59,6 +61,7 @@ export function FixtureTable({ robotType, mostRecentAnalysis ) + const deckDef = getDeckDefFromRobotType(robotType) const requiredDeckConfigCompatibility = getRequiredDeckConfig( deckConfigCompatibility @@ -96,6 +99,7 @@ export function FixtureTable({ setSetupScreen={setSetupScreen} setCutoutId={setCutoutId} setProvidedFixtureOptions={setProvidedFixtureOptions} + deckDef={deckDef} /> ) })} @@ -108,6 +112,7 @@ interface FixtureTableItemProps extends CutoutConfigAndCompatibility { setSetupScreen: React.Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void + deckDef: DeckDefinition } function FixtureTableItem({ @@ -119,6 +124,7 @@ function FixtureTableItem({ setSetupScreen, setCutoutId, setProvidedFixtureOptions, + deckDef, }: FixtureTableItemProps): JSX.Element { const { t, i18n } = useTranslation('protocol_setup') @@ -183,6 +189,7 @@ function FixtureTableItem({ requiredFixtureId={compatibleCutoutFixtureIds[0]} isOnDevice={true} missingLabwareDisplayName={missingLabwareDisplayName} + deckDef={deckDef} /> ) : null} > } export function ModuleTable(props: ModuleTableProps): JSX.Element { - const { - attachedProtocolModuleMatches, - deckDef, - protocolModulesInfo, - runId, - setShowMultipleModulesModal, - } = props + const { attachedProtocolModuleMatches, deckDef, runId } = props const { t } = useTranslation('protocol_setup') @@ -95,16 +85,6 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { {t('status')} {attachedProtocolModuleMatches.map(module => { - // check for duplicate module model in list of modules for protocol - const isDuplicateModuleModel = protocolModulesInfo - // filter out current module - .filter(otherModule => otherModule.moduleId !== module.moduleId) - // check for existence of another module of same model - .some( - otherModule => - otherModule.moduleDef.model === module.moduleDef.model - ) - const cutoutIdForSlotName = getCutoutIdForSlotName( module.slotName, deckDef @@ -134,14 +114,13 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { ) })} @@ -156,24 +135,22 @@ interface ModuleTableItemProps { continuePastCommandFailure: boolean ) => Promise conflictedFixture: CutoutConfig | null - isDuplicateModuleModel: boolean isLoading: boolean module: AttachedProtocolModuleMatch prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> - setShowMultipleModulesModal: React.Dispatch> + deckDef: DeckDefinition } function ModuleTableItem({ - isDuplicateModuleModel, module, - setShowMultipleModulesModal, calibrationStatus, chainLiveCommands, isLoading, prepCommandErrorMessage, setPrepCommandErrorMessage, conflictedFixture, + deckDef, }: ModuleTableItemProps): JSX.Element { const { i18n, t } = useTranslation(['protocol_setup', 'module_wizard_flows']) @@ -216,7 +193,6 @@ function ModuleTableItem({ background={false} iconName="connection-status" /> - {isDuplicateModuleModel ? : null} ) if (conflictedFixture != null) { @@ -231,7 +207,9 @@ function ModuleTableItem({ setShowLocationConflictModal(true)} + onClick={() => { + setShowLocationConflictModal(true) + }} /> ) @@ -246,17 +224,12 @@ function ModuleTableItem({ module.attachedModuleMatch?.moduleOffset?.last_modified != null ) { moduleStatus = ( - <> - - {isDuplicateModuleModel ? ( - - ) : null} - + ) } else if ( isModuleReady && @@ -285,8 +258,9 @@ function ModuleTableItem({ {showModuleWizard && module.attachedModuleMatch != null ? ( setShowModuleWizard(false)} - initialSlotName={module.slotName} + closeFlow={() => { + setShowModuleWizard(false) + }} isPrepCommandLoading={isLoading} prepCommandErrorMessage={ prepCommandErrorMessage === '' ? undefined : prepCommandErrorMessage @@ -295,9 +269,12 @@ function ModuleTableItem({ ) : null} {showLocationConflictModal && conflictedFixture != null ? ( setShowLocationConflictModal(false)} + onCloseClick={() => { + setShowLocationConflictModal(false) + }} cutoutId={conflictedFixture.cutoutId} requiredModule={module.moduleDef.model} + deckDef={deckDef} isOnDevice={true} /> ) : null} @@ -313,12 +290,9 @@ function ModuleTableItem({ : COLORS.yellow35 } borderRadius={BORDERS.borderRadius8} - cursor={isDuplicateModuleModel ? 'pointer' : 'inherit'} + cursor="inherit" gridGap={SPACING.spacing24} padding={`${SPACING.spacing16} ${SPACING.spacing24}`} - onClick={() => - isDuplicateModuleModel ? setShowMultipleModulesModal(true) : null - } > diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx index 7fbbf4f048e9..c7acb6f2a421 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/SetupInstructionsModal.tsx @@ -26,7 +26,7 @@ interface SetupInstructionsModalProps { export function SetupInstructionsModal({ setShowSetupInstructionsModal, }: SetupInstructionsModalProps): JSX.Element { - const { i18n, t } = useTranslation('protocol_setup') + const { i18n, t } = useTranslation(['protocol_setup', 'branded']) const modalHeader: ModalHeaderBaseProps = { title: i18n.format(t('setup_instructions'), 'capitalize'), iconName: 'information', @@ -45,7 +45,9 @@ export function SetupInstructionsModal({ gridGap={SPACING.spacing40} > - {t('setup_instructions_description')} + + {t('branded:setup_instructions_description')} + (false) const [ showSetupInstructionsModal, setShowSetupInstructionsModal, @@ -93,11 +88,6 @@ export function ProtocolSetupModulesAndDeck({ <> {createPortal( <> - {showMultipleModulesModal ? ( - setShowMultipleModulesModal(false)} - /> - ) : null} {showSetupInstructionsModal ? ( ) : null} void + parameter: RunTimeParameter + setParameter: (value: boolean | string | number, variableName: string) => void + rawValue: number | string | boolean +} + +export function ChooseEnum({ + handleGoBack, + parameter, + setParameter, + rawValue, +}: ChooseEnumProps): JSX.Element { + const { makeSnackbar } = useToaster() + + const { t } = useTranslation(['protocol_setup', 'shared']) + const options = 'choices' in parameter ? parameter.choices : null + const handleOnClick = (newValue: string | number | boolean): void => { + setParameter(newValue, parameter.variableName) + } + const resetValueDisabled = parameter.default === rawValue + + return ( + <> + + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParameter(parameter.default, parameter.variableName) + } + /> + + + {parameter.description} + + + {options?.map(option => { + return ( + handleOnClick(option.value)} + isSelected={option.value === rawValue} + /> + ) + })} + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx b/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx new file mode 100644 index 000000000000..da3c34a14c1c --- /dev/null +++ b/app/src/organisms/ProtocolSetupParameters/ChooseNumber.tsx @@ -0,0 +1,164 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { InputField } from '../../atoms/InputField' +import { useToaster } from '../ToasterOven' +import { ChildNavigation } from '../ChildNavigation' +import { NumericalKeyboard } from '../../atoms/SoftwareKeyboard' +import type { NumberParameter } from '@opentrons/shared-data' + +interface ChooseNumberProps { + handleGoBack: () => void + parameter: NumberParameter + setParameter: (value: number, variableName: string) => void +} + +export function ChooseNumber({ + handleGoBack, + parameter, + setParameter, +}: ChooseNumberProps): JSX.Element | null { + const { makeSnackbar } = useToaster() + + const { i18n, t } = useTranslation(['protocol_setup', 'shared']) + const keyboardRef = React.useRef(null) + const [paramValue, setParamValue] = React.useState( + String(parameter.value) + ) + + // We need to arbitrarily set the value of the keyboard to a string the + // same length as the initial parameter value (as string) when the component mounts + // so that the delete button operates properly on the exisiting input field value. + const [prevKeyboardValue, setPrevKeyboardValue] = React.useState('') + React.useEffect(() => { + const arbitraryInput = new Array(paramValue).join('*') + // @ts-expect-error keyboard should expose for `setInput` method + keyboardRef.current?.setInput(arbitraryInput) + setPrevKeyboardValue(arbitraryInput) + }, []) + + if (parameter.type !== 'int' && parameter.type !== 'float') { + console.log(`Incorrect parameter type: ${parameter.type}`) + return null + } + const handleClickGoBack = (newValue: number): void => { + if (error != null) { + makeSnackbar(t('value_out_of_range_generic')) + } else { + setParameter(newValue, parameter.variableName) + handleGoBack() + } + } + + const handleKeyboardInput = (e: string): void => { + if (prevKeyboardValue.length < e.length) { + const lastDigit = e.slice(-1) + if ( + !'.-'.includes(lastDigit) || + (lastDigit === '.' && !paramValue.includes('.')) || + (lastDigit === '-' && paramValue.length === 0) + ) { + setParamValue(paramValue + lastDigit) + } + } else { + setParamValue(paramValue.slice(0, paramValue.length - 1)) + } + setPrevKeyboardValue(e) + } + + const paramValueAsNumber = Number(paramValue) + const resetValueDisabled = parameter.default === paramValueAsNumber + const { min, max } = parameter + const error = + paramValue === '' || + Number.isNaN(paramValueAsNumber) || + paramValueAsNumber < min || + paramValueAsNumber > max + ? t(`value_out_of_range`, { + min: parameter.type === 'int' ? min : min.toFixed(1), + max: parameter.type === 'int' ? max : max.toFixed(1), + }) + : null + + return ( + <> + { + handleClickGoBack(paramValueAsNumber) + }} + buttonType="tertiaryLowLight" + buttonText={t('restore_default')} + onClickButton={() => + resetValueDisabled + ? makeSnackbar(t('no_custom_values')) + : setParamValue(String(parameter.default)) + } + /> + + + + {parameter.description} + + { + const updatedValue = + parameter.type === 'int' + ? Math.round(e.target.valueAsNumber) + : e.target.valueAsNumber + setParamValue( + Number.isNaN(updatedValue) ? '' : String(updatedValue) + ) + }} + /> + + + { + handleKeyboardInput(e) + }} + /> + + + + ) +} diff --git a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx index 458b1172f3a1..b49151f883bd 100644 --- a/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ResetValuesModal.tsx @@ -14,13 +14,18 @@ import { import { SmallButton } from '../../atoms/buttons' import { Modal } from '../../molecules/Modal' +import type { RunTimeParameter } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' interface ResetValuesModalProps { + runTimeParametersOverrides: RunTimeParameter[] + setRunTimeParametersOverrides: (parameters: RunTimeParameter[]) => void handleGoBack: () => void } export function ResetValuesModal({ + runTimeParametersOverrides, + setRunTimeParametersOverrides, handleGoBack, }: ResetValuesModalProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'shared']) @@ -33,7 +38,12 @@ export function ResetValuesModal({ // ToDo (kk:03/18/2024) reset values function will be implemented const handleResetValues = (): void => { - console.log('todo add reset values function') + setRunTimeParametersOverrides( + runTimeParametersOverrides.map(param => { + return { ...param, value: param.default } + }) + ) + handleGoBack() } const modalProps = { diff --git a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx index e8aca7d8c9c0..3ce9169f77f2 100644 --- a/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx +++ b/app/src/organisms/ProtocolSetupParameters/ViewOnlyParameters.tsx @@ -16,7 +16,6 @@ import { import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ChildNavigation } from '../ChildNavigation' import { useToaster } from '../ToasterOven' -import { mockData } from './index' import type { SetupScreens } from '../../pages/ProtocolSetup' @@ -36,8 +35,7 @@ export function ViewOnlyParameters({ makeSnackbar(t('reset_setup')) } - // TODO(jr, 3/18/24): remove mockData - const parameters = mostRecentAnalysis?.runTimeParameters ?? mockData + const parameters = mostRecentAnalysis?.runTimeParameters ?? [] return ( <> @@ -68,9 +66,6 @@ export function ViewOnlyParameters({ {t('value')} {parameters.map((parameter, index) => { - // TODO(jr, 3/20/24): plug in the info if the - // parameter changed from the default - const hasCustomValue = true return ( - + {formatRunTimeParameterValue(parameter, t)} - {hasCustomValue ? ( + {parameter.value !== parameter.default ? ( ) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +describe('ChooseEnum', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + setParameter: vi.fn(), + handleGoBack: vi.fn(), + parameter: { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + value: 'none', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + rawValue: '1', + } + }) + it('renders the back icon and calls the prop', () => { + render(props) + fireEvent.click(screen.getAllByRole('button')[0]) + expect(props.handleGoBack).toHaveBeenCalled() + }) + it('calls the prop if reset default is clicked when the default has changed', () => { + render(props) + fireEvent.click(screen.getByText('Restore default value')) + expect(props.setParameter).toHaveBeenCalled() + }) + it('calls does not call prop if reset default is clicked when the default has not changed', () => { + props = { + ...props, + rawValue: 'none', + } + render(props) + fireEvent.click(screen.getByText('Restore default value')) + expect(props.setParameter).not.toHaveBeenCalled() + }) + it('should render the text and buttons for choice param', () => { + render(props) + screen.getByText('no offsets') + screen.getByText('temp offset') + screen.getByText('heater-shaker offset') + const notSelectedOption = screen.getByRole('label', { name: 'no offsets' }) + const selectedOption = screen.getByRole('label', { + name: 'temp offset', + }) + expect(notSelectedOption).toHaveStyle(`background-color: ${COLORS.blue40}`) + expect(selectedOption).toHaveStyle(`background-color: ${COLORS.blue60}`) + }) +}) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx index 4873745356c9..4871eeaa379e 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ProtocolSetupParameters.test.tsx @@ -2,16 +2,22 @@ import * as React from 'react' import { when } from 'vitest-when' import { it, describe, beforeEach, vi, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { renderWithProviders } from '../../../__testing-utils__' import { ProtocolSetupParameters } from '..' +import { ChooseEnum } from '../ChooseEnum' import { mockRunTimeParameterData } from '../../../pages/ProtocolDetails/fixtures' import type * as ReactRouterDom from 'react-router-dom' import type { HostConfig } from '@opentrons/api-client' const mockGoBack = vi.fn() +vi.mock('../ChooseEnum') vi.mock('@opentrons/react-api-client') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('react-router-dom', async importOriginal => { @@ -22,6 +28,7 @@ vi.mock('react-router-dom', async importOriginal => { } }) const MOCK_HOST_CONFIG: HostConfig = { hostname: 'MOCK_HOST' } +const mockCreateProtocolAnalysis = vi.fn() const mockCreateRun = vi.fn() const render = ( props: React.ComponentProps @@ -39,7 +46,11 @@ describe('ProtocolSetupParameters', () => { labwareOffsets: [], runTimeParameters: mockRunTimeParameterData, } + vi.mocked(ChooseEnum).mockReturnValue(
mock ChooseEnum
) vi.mocked(useHost).mockReturnValue(MOCK_HOST_CONFIG) + when(vi.mocked(useCreateProtocolAnalysisMutation)) + .calledWith(expect.anything(), expect.anything()) + .thenReturn({ createProtocolAnalysis: mockCreateProtocolAnalysis } as any) when(vi.mocked(useCreateRunMutation)) .calledWith(expect.anything()) .thenReturn({ createRun: mockCreateRun } as any) @@ -52,6 +63,17 @@ describe('ProtocolSetupParameters', () => { screen.getByText('Dry Run') screen.getByText('a dry run description') }) + it('renders the ChooseEnum component when a str param is selected', () => { + render(props) + fireEvent.click(screen.getByText('Default Module Offsets')) + screen.getByText('mock ChooseEnum') + }) + it('renders the other setting when boolean param is selected', () => { + render(props) + expect(screen.getAllByText('On')).toHaveLength(2) + fireEvent.click(screen.getByText('Dry Run')) + expect(screen.getAllByText('On')).toHaveLength(3) + }) it('renders the back icon and calls useHistory', () => { render(props) fireEvent.click(screen.getAllByRole('button')[0]) @@ -73,6 +95,5 @@ describe('ProtocolSetupParameters', () => { const title = screen.getByText('Reset parameter values?') fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(title).not.toBeInTheDocument() - // TODO(jr, 3/19/24): wire up the confirm button }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx index a8f876b94f3f..ec2eb28a81c5 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ResetValuesModal.test.tsx @@ -5,8 +5,10 @@ import { fireEvent, screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ResetValuesModal } from '../ResetValuesModal' +import { RunTimeParameter } from '@opentrons/shared-data' const mockGoBack = vi.fn() +const mockSetRunTimeParametersOverrides = vi.fn() const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -19,6 +21,8 @@ describe('ResetValuesModal', () => { beforeEach(() => { props = { + runTimeParametersOverrides: [] as RunTimeParameter[], + setRunTimeParametersOverrides: mockSetRunTimeParametersOverrides, handleGoBack: mockGoBack, } }) @@ -42,5 +46,11 @@ describe('ResetValuesModal', () => { }) // ToDo (kk: 03/18/2024) reset value button test will be added - it.todo('should call a mock function when tapping reset values button') + it('should call a mock function when tapping reset values button', () => { + render(props) + const resetValuesButton = screen.getByText('Reset values') + fireEvent.click(resetValuesButton) + expect(mockSetRunTimeParametersOverrides) + expect(mockGoBack).toHaveBeenCalled() + }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx b/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx index 90893117b6f8..6e20fe656581 100644 --- a/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx +++ b/app/src/organisms/ProtocolSetupParameters/__tests__/ViewOnlyParameters.test.tsx @@ -60,6 +60,8 @@ describe('ViewOnlyParameters', () => { fireEvent.click(screen.getAllByRole('button')[0]) expect(props.setSetupScreen).toHaveBeenCalled() }) - // TODO(jr, 3/20/24):test the update chip when - // custom value boolean is wired up + it('renders chip for updated values', () => { + render(props) + screen.getByTestId('Chip_USE_GRIPPER') + }) }) diff --git a/app/src/organisms/ProtocolSetupParameters/index.tsx b/app/src/organisms/ProtocolSetupParameters/index.tsx index c99c4ebeff60..5dae07260f6c 100644 --- a/app/src/organisms/ProtocolSetupParameters/index.tsx +++ b/app/src/organisms/ProtocolSetupParameters/index.tsx @@ -1,169 +1,33 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useHistory } from 'react-router-dom' -import { useCreateRunMutation, useHost } from '@opentrons/react-api-client' +import { + useCreateProtocolAnalysisMutation, + useCreateRunMutation, + useHost, +} from '@opentrons/react-api-client' import { useQueryClient } from 'react-query' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ALIGN_CENTER, DIRECTION_COLUMN, Flex, SPACING, } from '@opentrons/components' +import { formatRunTimeParameterValue } from '@opentrons/shared-data' import { ProtocolSetupStep } from '../../pages/ProtocolSetup' +import { getRunTimeParameterValuesForRun } from '../Devices/utils' import { ChildNavigation } from '../ChildNavigation' import { ResetValuesModal } from './ResetValuesModal' +import { ChooseEnum } from './ChooseEnum' +import { ChooseNumber } from './ChooseNumber' -import type { RunTimeParameter } from '@opentrons/shared-data' +import type { NumberParameter, RunTimeParameter } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' -export const mockData: RunTimeParameter[] = [ - { - value: false, - displayName: 'Dry Run', - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', - default: false, - }, - { - value: true, - displayName: 'Use Gripper', - variableName: 'USE_GRIPPER', - description: 'For using the gripper.', - type: 'boolean', - default: true, - }, - { - value: true, - displayName: 'Trash Tips', - variableName: 'TIP_TRASH', - description: - 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', - default: true, - }, - { - value: true, - displayName: 'Deactivate Temperatures', - variableName: 'DEACTIVATE_TEMP', - description: 'deactivate temperature on the module', - type: 'boolean', - default: true, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6, - displayName: 'PCR Cycles', - variableName: 'PCR_CYCLES', - description: 'number of PCR cycles on a thermocycler', - type: 'int', - min: 1, - max: 10, - default: 6, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - { - value: 'left', - displayName: 'pipette mount', - variableName: 'mont', - description: 'pipette mount', - type: 'str', - choices: [ - { - displayName: 'Left', - value: 'left', - }, - { - displayName: 'Right', - value: 'right', - }, - ], - default: 'left', - }, - { - value: 'flex', - displayName: 'short test case', - variableName: 'short 2 options', - description: 'this play 2 short options', - type: 'str', - choices: [ - { - displayName: 'OT-2', - value: 'ot2', - }, - { - displayName: 'Flex', - value: 'flex', - }, - ], - default: 'flex', - }, - { - value: 'flex', - displayName: 'long test case', - variableName: 'long 2 options', - description: 'this play 2 long options', - type: 'str', - choices: [ - { - displayName: 'I am kind of long text version', - value: 'ot2', - }, - { - displayName: 'I am kind of long text version. Today is 3/15', - value: 'flex', - }, - ], - default: 'flex', - }, -] - interface ProtocolSetupParametersProps { protocolId: string - runTimeParameters?: RunTimeParameter[] + runTimeParameters: RunTimeParameter[] labwareOffsets?: LabwareOffsetCreateData[] } @@ -176,11 +40,68 @@ export function ProtocolSetupParameters({ const history = useHistory() const host = useHost() const queryClient = useQueryClient() + const [ + chooseValueScreen, + setChooseValueScreen, + ] = React.useState(null) + const [ + showNumericalInputScreen, + setShowNumericalInputScreen, + ] = React.useState(null) const [resetValuesModal, showResetValuesModal] = React.useState( false ) - const parameters = runTimeParameters ?? [] - // TODO(jr, 3/20/24): modify useCreateRunMutation to take in optional run time parameters + const [startSetup, setStartSetup] = React.useState(false) + const [ + runTimeParametersOverrides, + setRunTimeParametersOverrides, + ] = React.useState( + // present defaults rather than last-set value + runTimeParameters.map(param => { + return { ...param, value: param.default } + }) + ) + + const updateParameters = ( + value: boolean | string | number, + variableName: string + ): void => { + const updatedParameters = runTimeParametersOverrides.map(parameter => { + if (parameter.variableName === variableName) { + return { ...parameter, value } + } + return parameter + }) + setRunTimeParametersOverrides(updatedParameters) + if (chooseValueScreen && chooseValueScreen.variableName === variableName) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setChooseValueScreen(updatedParameter) + } + } + if ( + showNumericalInputScreen && + showNumericalInputScreen.variableName === variableName + ) { + const updatedParameter = updatedParameters.find( + parameter => parameter.variableName === variableName + ) + if (updatedParameter != null) { + setShowNumericalInputScreen(updatedParameter as NumberParameter) + } + } + } + + const runTimeParameterValues = getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ) + const { createProtocolAnalysis } = useCreateProtocolAnalysisMutation( + protocolId, + host + ) + const { createRun, isLoading } = useCreateRunMutation({ onSuccess: data => { queryClient @@ -191,25 +112,45 @@ export function ProtocolSetupParameters({ }, }) const handleConfirmValues = (): void => { - createRun({ protocolId, labwareOffsets }) + setStartSetup(true) + createProtocolAnalysis({ + protocolKey: protocolId, + runTimeParameterValues: runTimeParameterValues, + }) + createRun({ + protocolId, + labwareOffsets, + runTimeParameterValues: getRunTimeParameterValuesForRun( + runTimeParametersOverrides + ), + }) } - return ( - <> - {resetValuesModal ? ( - showResetValuesModal(false)} /> - ) : null} + const handleSetParameter = (parameter: RunTimeParameter): void => { + if ('choices' in parameter) { + setChooseValueScreen(parameter) + } else if (parameter.type === 'bool') { + updateParameters(!parameter.value, parameter.variableName) + } else if (parameter.type === 'int' || parameter.type === 'float') { + setShowNumericalInputScreen(parameter) + } else { + // bad param + console.log('error') + } + } + let children = ( + <> history.goBack()} onClickButton={handleConfirmValues} buttonText={t('confirm_values')} - iconName={isLoading ? 'ot-spinner' : undefined} + iconName={isLoading || startSetup ? 'ot-spinner' : undefined} iconPlacement="startIcon" secondaryButtonProps={{ buttonType: 'tertiaryLowLight', - buttonText: t('restore_default'), + buttonText: t('restore_defaults'), onClick: () => showResetValuesModal(true), }} /> @@ -218,16 +159,17 @@ export function ProtocolSetupParameters({ alignItems={ALIGN_CENTER} flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8} - paddingX={SPACING.spacing8} + paddingX={SPACING.spacing40} + paddingBottom={SPACING.spacing40} > - {parameters.map((parameter, index) => { + {runTimeParametersOverrides.map((parameter, index) => { return ( console.log('TODO: wire this up')} + onClickSetupStep={() => handleSetParameter(parameter)} detail={formatRunTimeParameterValue(parameter, t)} description={parameter.description} fontSize="h4" @@ -238,4 +180,36 @@ export function ProtocolSetupParameters({
) + if (chooseValueScreen != null) { + children = ( + setChooseValueScreen(null)} + parameter={chooseValueScreen} + setParameter={updateParameters} + rawValue={chooseValueScreen.value} + /> + ) + } + if (showNumericalInputScreen != null) { + children = ( + setShowNumericalInputScreen(null)} + parameter={showNumericalInputScreen} + setParameter={updateParameters} + /> + ) + } + + return ( + <> + {resetValuesModal ? ( + showResetValuesModal(false)} + /> + ) : null} + {children} + + ) } diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 4f4fb33ab001..af388d30930d 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -30,6 +30,7 @@ describe('useCloneRun hook', () => { id: RUN_ID, protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }, }, } as any) @@ -60,6 +61,7 @@ describe('useCloneRun hook', () => { expect(mockCreateRun).toHaveBeenCalledWith({ protocolId: 'protocolId', labwareOffsets: 'someOffset', + runTimeParameterValues: 'someRtp', }) }) }) diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index c7ba887ab549..0858544d93cd 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -30,8 +30,12 @@ export function useCloneRun( }) const cloneRun = (): void => { if (runRecord != null) { - const { protocolId, labwareOffsets } = runRecord.data - createRun({ protocolId, labwareOffsets }) + const { + protocolId, + labwareOffsets, + runTimeParameterValues, + } = runRecord.data + createRun({ protocolId, labwareOffsets, runTimeParameterValues }) } else { console.info('failed to clone run record, source run record not found') } diff --git a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx index 49243c2b7901..cfba24021721 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx +++ b/app/src/organisms/ProtocolsLanding/__tests__/hooks.test.tsx @@ -97,6 +97,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -183,6 +184,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, @@ -273,6 +275,7 @@ const mockStoredProtocolData = [ displayColor: '#ff4f4f', }, ], + runTimeParameters: [], errors: [], } as ProtocolAnalysisOutput, }, diff --git a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts index e4383c842b98..1ff0d74f72ac 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts +++ b/app/src/organisms/ProtocolsLanding/__tests__/utils.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest' -import { getisFlexProtocol, getRobotTypeDisplayName } from '../utils' +import { + getAnalysisStatus, + getisFlexProtocol, + getRobotTypeDisplayName, +} from '../utils' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' const mockOT3ProtocolAnalysisOutput = { @@ -10,6 +14,36 @@ const mockOT2ProtocolAnalysisOutput = { robotType: 'OT-2 Standard', } as ProtocolAnalysisOutput +describe('getAnalysisStatus', () => { + it('should return stale if no liquids in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return stale if no runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('stale') + }) + + it('should return complete if liquids and runTimeParameters in analysis', () => { + const result = getAnalysisStatus(false, { + ...mockOT3ProtocolAnalysisOutput, + liquids: [], + runTimeParameters: [], + errors: [], + }) + expect(result).toBe('complete') + }) +}) + describe('getisFlexProtocol', () => { it('should return true for protocols intended for a Flex', () => { const result = getisFlexProtocol(mockOT3ProtocolAnalysisOutput) diff --git a/app/src/organisms/ProtocolsLanding/utils.ts b/app/src/organisms/ProtocolsLanding/utils.ts index 59ccfc2e852d..dfc9b4fafc33 100644 --- a/app/src/organisms/ProtocolsLanding/utils.ts +++ b/app/src/organisms/ProtocolsLanding/utils.ts @@ -10,7 +10,10 @@ export function getAnalysisStatus( ): AnalysisStatus { if (isAnalyzing) { return 'loading' - } else if (analysis != null && analysis?.liquids == null) { + } else if ( + analysis != null && + (analysis.liquids == null || analysis.runTimeParameters == null) + ) { return 'stale' } else if (analysis != null) { return analysis.errors.length > 0 ? 'error' : 'complete' diff --git a/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx new file mode 100644 index 000000000000..57d6ce14b549 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx @@ -0,0 +1,74 @@ +import * as React from 'react' +import { useTranslation, Trans } from 'react-i18next' +import { + Flex, + SPACING, + StyledText, + DeckConfigurator, + TYPOGRAPHY, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { SmallButton } from '../../atoms/buttons' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' +import { ChildNavigation } from '../ChildNavigation' + +interface CreateNewTransferProps { + onNext: () => void + exitButtonProps: React.ComponentProps +} + +export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element { + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const deckConfig = useDeckConfigurationQuery().data ?? [] + return ( + + + + + + + ), + }} + /> + + + {}} + handleClickRemove={() => {}} + additionalStaticFixtures={[ + { location: 'cutoutB2', label: t('tip_rack') }, + { location: 'cutoutC2', label: t('labware') }, + { location: 'cutoutD2', label: t('labware') }, + ]} + /> + + + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx new file mode 100644 index 000000000000..0f92ca0d508c --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/SelectPipette.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + Flex, + SPACING, + StyledText, + TYPOGRAPHY, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { getPipetteSpecsV2, RIGHT, LEFT } from '@opentrons/shared-data' +import { SmallButton, LargeButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' + +import type { PipetteData, Mount } from '@opentrons/api-client' +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +interface SelectPipetteProps { + onNext: () => void + onBack: () => void + exitButtonProps: React.ComponentProps + state: QuickTransferSetupState + dispatch: React.Dispatch +} + +export function SelectPipette(props: SelectPipetteProps): JSX.Element { + const { onNext, onBack, exitButtonProps, state, dispatch } = props + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const { data: attachedInstruments } = useInstrumentsQuery() + + const leftPipette = attachedInstruments?.data.find( + (i): i is PipetteData => i.ok && i.mount === LEFT + ) + const leftPipetteSpecs = + leftPipette != null ? getPipetteSpecsV2(leftPipette.instrumentModel) : null + + const rightPipette = attachedInstruments?.data.find( + (i): i is PipetteData => i.ok && i.mount === RIGHT + ) + const rightPipetteSpecs = + rightPipette != null + ? getPipetteSpecsV2(rightPipette.instrumentModel) + : null + + // automatically select 96 channel if it is attached + const [selectedPipette, setSelectedPipette] = React.useState< + Mount | undefined + >(leftPipetteSpecs?.channels === 96 ? LEFT : state.mount) + + const handleClickNext = (): void => { + const selectedPipetteSpecs = + selectedPipette === LEFT ? leftPipetteSpecs : rightPipetteSpecs + + // the button will be disabled if these values are null + if (selectedPipette != null && selectedPipetteSpecs != null) { + dispatch({ + type: 'SELECT_PIPETTE', + pipette: selectedPipetteSpecs, + mount: selectedPipette, + }) + onNext() + } + } + return ( + + + + + {t('pipette_currently_attached')} + + {leftPipetteSpecs != null ? ( + { + setSelectedPipette(LEFT) + }} + buttonText={ + leftPipetteSpecs.channels === 96 + ? t('both_mounts') + : t('left_mount') + } + subtext={leftPipetteSpecs.displayName} + /> + ) : null} + {rightPipetteSpecs != null ? ( + { + setSelectedPipette(RIGHT) + }} + buttonText={t('right_mount')} + subtext={rightPipetteSpecs.displayName} + /> + ) : null} + + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx new file mode 100644 index 000000000000..abeba9a2b1da --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { DeckConfigurator } from '@opentrons/components' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { CreateNewTransfer } from '../CreateNewTransfer' + +import type * as OpentronsComponents from '@opentrons/components' + +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + DeckConfigurator: vi.fn(), + } +}) +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('CreateNewTransfer', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + } + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the create new transfer screen and header', () => { + render(props) + screen.getByText('Create new quick transfer') + screen.getByText( + 'Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.' + ) + screen.getByText( + 'Make sure that your deck configuration is up to date to avoid collisions.' + ) + expect(vi.mocked(DeckConfigurator)).toHaveBeenCalled() + }) + it('renders exit and continue buttons and they work as expected', () => { + render(props) + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx new file mode 100644 index 000000000000..2d6faa6ffa7f --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/SelectPipette.test.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' +import { useInstrumentsQuery } from '@opentrons/react-api-client' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { SelectPipette } from '../SelectPipette' + +vi.mock('@opentrons/react-api-client') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SelectPipette', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onNext: vi.fn(), + onBack: vi.fn(), + exitButtonProps: { + buttonType: 'tertiaryLowLight', + buttonText: 'Exit', + onClick: vi.fn(), + }, + state: {}, + dispatch: vi.fn(), + } + vi.mocked(useInstrumentsQuery).mockReturnValue({ + data: { + data: [ + { + instrumentType: 'pipette', + mount: 'left', + ok: true, + firmwareVersion: 12, + instrumentName: 'p10_single', + instrumentModel: 'p1000_multi_v3.4', + data: {}, + } as any, + { + instrumentType: 'pipette', + mount: 'right', + ok: true, + firmwareVersion: 12, + instrumentName: 'p10_single', + instrumentModel: 'p1000_multi_v3.4', + data: {}, + } as any, + ], + }, + } as any) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the select pipette screen, header, and exit button', () => { + render(props) + screen.getByText('Select attached pipette') + screen.getByText( + 'Quick transfer options depend on the pipettes currently attached to your robot.' + ) + const exitBtn = screen.getByText('Exit') + fireEvent.click(exitBtn) + expect(props.exitButtonProps.onClick).toHaveBeenCalled() + }) + + it('renders continue button and it is disabled if no pipette is selected', () => { + render(props) + screen.getByText('Continue') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + }) + + it('renders both pipette buttons if there are two attached', () => { + render(props) + screen.getByText('Left Mount') + screen.getByText('Right Mount') + }) + + it('selects pipette by default if there is one in state, button will be enabled', () => { + render({ ...props, state: { mount: 'left' } }) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.onNext).toHaveBeenCalled() + }) + + it('enables continue button if you click a pipette', () => { + render(props) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeDisabled() + const leftButton = screen.getByText('Left Mount') + fireEvent.click(leftButton) + expect(continueBtn).toBeEnabled() + fireEvent.click(continueBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(props.onNext).toHaveBeenCalled() + }) + + it('renders left and right button if 96 is attached and automatically selects the pipette', () => { + vi.mocked(useInstrumentsQuery).mockReturnValue({ + data: { + data: [ + { + instrumentType: 'pipette', + mount: 'left', + ok: true, + firmwareVersion: 12, + instrumentName: 'p1000_96', + instrumentModel: 'p1000_96_v1', + data: {}, + } as any, + ], + }, + } as any) + render(props) + screen.getByText('Left + Right Mount') + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(continueBtn).toBeEnabled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/constants.ts b/app/src/organisms/QuickTransferFlow/constants.ts new file mode 100644 index 000000000000..3241759a0443 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/constants.ts @@ -0,0 +1,9 @@ +export const ACTIONS = { + SELECT_PIPETTE: 'SELECT_PIPETTE', + SELECT_TIP_RACK: 'SELECT_TIP_RACK', + SET_SOURCE_LABWARE: 'SET_SOURCE_LABWARE', + SET_SOURCE_WELLS: 'SET_SOURCE_WELLS', + SET_DEST_LABWARE: 'SET_DEST_LABWARE', + SET_DEST_WELLS: 'SET_DEST_WELLS', + SET_VOLUME: 'SET_VOLUME', +} as const diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx new file mode 100644 index 000000000000..36d0175b0db9 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/index.tsx @@ -0,0 +1,116 @@ +import * as React from 'react' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Flex, StepMeter, SPACING } from '@opentrons/components' +import { SmallButton } from '../../atoms/buttons' +import { ChildNavigation } from '../ChildNavigation' +import { CreateNewTransfer } from './CreateNewTransfer' +import { SelectPipette } from './SelectPipette' +import { quickTransferReducer } from './utils' + +import type { QuickTransferSetupState } from './types' + +const QUICK_TRANSFER_WIZARD_STEPS = 8 +const initialQuickTransferState: QuickTransferSetupState = {} + +export const QuickTransferFlow = (): JSX.Element => { + const history = useHistory() + const { i18n, t } = useTranslation(['quick_transfer', 'shared']) + const [state, dispatch] = React.useReducer( + quickTransferReducer, + initialQuickTransferState + ) + const [currentStep, setCurrentStep] = React.useState(1) + const [continueIsDisabled] = React.useState(false) + + // every child component will take state as a prop, an anonymous + // dispatch function related to that step (except create new), + // and a function to disable the continue button + + const exitButtonProps: React.ComponentProps = { + buttonType: 'tertiaryLowLight', + buttonText: i18n.format(t('shared:exit'), 'capitalize'), + onClick: () => { + history.push('protocols') + }, + } + + // these will be moved to the child components once they all exist + const ORDERED_STEP_HEADERS: string[] = [ + t('create_new_transfer'), + t('select_attached_pipette'), + t('select_tip_rack'), + t('select_source_labware'), + t('select_source_wells'), + t('select_dest_labware'), + t('select_dest_wells'), + t('set_transfer_volume'), + ] + + const header = ORDERED_STEP_HEADERS[currentStep - 1] + let modalContent: JSX.Element | null = null + if (currentStep === 1) { + modalContent = ( + setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) + } else if (currentStep === 2) { + modalContent = ( + setCurrentStep(prevStep => prevStep - 1)} + onNext={() => setCurrentStep(prevStep => prevStep + 1)} + exitButtonProps={exitButtonProps} + /> + ) + } else { + modalContent = null + } + + // until each page is wired up, show header title with empty screen + return ( + <> + + {modalContent == null ? ( + + { + setCurrentStep(prevStep => prevStep - 1) + } + } + buttonText={i18n.format(t('shared:continue'), 'capitalize')} + onClickButton={() => { + if (currentStep === 8) { + history.push('protocols') + } else { + setCurrentStep(prevStep => prevStep + 1) + } + }} + buttonIsDisabled={continueIsDisabled} + secondaryButtonProps={{ + buttonType: 'tertiaryLowLight', + buttonText: i18n.format(t('shared:exit'), 'capitalize'), + onClick: () => { + history.push('protocols') + }, + }} + top={SPACING.spacing8} + /> + {modalContent} + + ) : ( + modalContent + )} + + ) +} diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts new file mode 100644 index 000000000000..814dae22a71a --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/types.ts @@ -0,0 +1,53 @@ +import { ACTIONS } from './constants' +import type { Mount } from '@opentrons/api-client' +import type { LabwareDefinition1, PipetteV2Specs } from '@opentrons/shared-data' + +export interface QuickTransferSetupState { + pipette?: PipetteV2Specs + mount?: Mount + tipRack?: LabwareDefinition1 + source?: LabwareDefinition1 + sourceWells?: string[] + destination?: LabwareDefinition1 + destinationWells?: string[] + volume?: number +} + +export type QuickTransferWizardAction = + | SelectPipetteAction + | SelectTipRackAction + | SetSourceLabwareAction + | SetSourceWellsAction + | SetDestLabwareAction + | SetDestWellsAction + | SetVolumeAction + +interface SelectPipetteAction { + type: typeof ACTIONS.SELECT_PIPETTE + mount: Mount + pipette: PipetteV2Specs +} +interface SelectTipRackAction { + type: typeof ACTIONS.SELECT_TIP_RACK + tipRack: LabwareDefinition1 +} +interface SetSourceLabwareAction { + type: typeof ACTIONS.SET_SOURCE_LABWARE + labware: LabwareDefinition1 +} +interface SetSourceWellsAction { + type: typeof ACTIONS.SET_SOURCE_WELLS + wells: string[] +} +interface SetDestLabwareAction { + type: typeof ACTIONS.SET_DEST_LABWARE + labware: LabwareDefinition1 +} +interface SetDestWellsAction { + type: typeof ACTIONS.SET_DEST_WELLS + wells: string[] +} +interface SetVolumeAction { + type: typeof ACTIONS.SET_VOLUME + volume: number +} diff --git a/app/src/organisms/QuickTransferFlow/utils.ts b/app/src/organisms/QuickTransferFlow/utils.ts new file mode 100644 index 000000000000..ee13d4c1720d --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/utils.ts @@ -0,0 +1,75 @@ +import type { + QuickTransferSetupState, + QuickTransferWizardAction, +} from './types' + +export function quickTransferReducer( + state: QuickTransferSetupState, + action: QuickTransferWizardAction +): QuickTransferSetupState { + switch (action.type) { + case 'SELECT_PIPETTE': { + return { + pipette: action.pipette, + mount: action.mount, + } + } + case 'SELECT_TIP_RACK': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: action.tipRack, + } + } + case 'SET_SOURCE_LABWARE': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: state.tipRack, + source: action.labware, + } + } + case 'SET_SOURCE_WELLS': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: state.tipRack, + source: state.source, + sourceWells: action.wells, + } + } + case 'SET_DEST_LABWARE': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: action.labware, + } + } + case 'SET_DEST_WELLS': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: state.destination, + destinationWells: action.wells, + } + } + case 'SET_VOLUME': { + return { + pipette: state.pipette, + mount: state.mount, + tipRack: state.tipRack, + source: state.source, + sourceWells: state.sourceWells, + destination: state.destination, + destinationWells: state.destinationWells, + volume: action.volume, + } + } + } +} diff --git a/app/src/organisms/RobotSettingsDashboard/Privacy.tsx b/app/src/organisms/RobotSettingsDashboard/Privacy.tsx index ceca1dea718a..7f8963b15e8f 100644 --- a/app/src/organisms/RobotSettingsDashboard/Privacy.tsx +++ b/app/src/organisms/RobotSettingsDashboard/Privacy.tsx @@ -32,7 +32,7 @@ export function Privacy({ robotName, setCurrentOption, }: PrivacyProps): JSX.Element { - const { t } = useTranslation('app_settings') + const { t } = useTranslation(['app_settings', 'branded']) const dispatch = useDispatch() const allRobotSettings = useSelector((state: State) => @@ -62,7 +62,7 @@ export function Privacy({ lineHeight={TYPOGRAPHY.lineHeight36} fontWeight={TYPOGRAPHY.fontWeightRegular} > - {t('opentrons_cares_about_privacy')} + {t('branded:opentrons_cares_about_privacy')}
} onClick={() => dispatch(toggleAnalyticsOptedIn())} diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx index e1fffe74e30b..8e2a8675f18a 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx +++ b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersionModal.tsx @@ -51,7 +51,7 @@ export function RobotSystemVersionModal({ > diff --git a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx index 172d8b153949..809ee0eee888 100644 --- a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx +++ b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx @@ -22,7 +22,7 @@ import { useStopRunMutation } from '@opentrons/react-api-client' import { getModalPortalEl } from '../../App/portal' import { LegacyModal } from '../../molecules/LegacyModal' -import { useTrackProtocolRunEvent } from '../Devices/hooks' +import { useTrackProtocolRunEvent, useIsFlex } from '../Devices/hooks' import { useRunStatus } from '../RunTimeControl/hooks' import { ANALYTICS_PROTOCOL_RUN_CANCEL } from '../../redux/analytics' @@ -39,9 +39,14 @@ export function ConfirmCancelModal( const { stopRun } = useStopRunMutation() const [isCanceling, setIsCanceling] = React.useState(false) const runStatus = useRunStatus(runId) + const isFlex = useIsFlex(robotName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const { t } = useTranslation('run_details') + const cancelRunAlertInfo = isFlex + ? t('cancel_run_alert_info_flex') + : t('cancel_run_alert_info_ot2') + const cancelRun: React.MouseEventHandler = (e): void => { e.preventDefault() e.stopPropagation() @@ -72,7 +77,7 @@ export function ConfirmCancelModal( > - {t('cancel_run_alert_info')} + {cancelRunAlertInfo} {t('cancel_run_module_info')} diff --git a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx b/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx index 872a23b8daa4..92fed1f5e4f6 100644 --- a/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx +++ b/app/src/organisms/RunDetails/__tests__/ConfirmCancelModal.test.tsx @@ -11,7 +11,10 @@ import { import { useStopRunMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' -import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' +import { + useIsFlex, + useTrackProtocolRunEvent, +} from '../../../organisms/Devices/hooks' import { useTrackEvent } from '../../../redux/analytics' import { renderWithProviders } from '../../../__testing-utils__' import { ConfirmCancelModal } from '../../../organisms/RunDetails/ConfirmCancelModal' @@ -56,6 +59,7 @@ describe('ConfirmCancelModal', () => { when(useTrackProtocolRunEvent).calledWith(RUN_ID, ROBOT_NAME).thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) + vi.mocked(useIsFlex).mockReturnValue(true) props = { onClose: vi.fn(), runId: RUN_ID, robotName: ROBOT_NAME } }) @@ -66,15 +70,20 @@ describe('ConfirmCancelModal', () => { it('should render the correct title', () => { render(props) - screen.getByText('Are you sure you want to cancel this run?') + screen.getByText('Are you sure you want to cancel?') }) - it('should render the correct body', () => { + it('should render the correct body text for a Flex', () => { render(props) + screen.getByText('Doing so will terminate this run and home your robot.') screen.getByText( - 'Doing so will terminate this run, drop any attached tips in the trash container and home your robot.' + 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' ) + }) + it('should render correct alternative body text for an OT-2', () => { + vi.mocked(useIsFlex).mockReturnValue(false) + render(props) screen.getByText( - 'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.' + 'Doing so will terminate this run, drop any attached tips in the trash container, and home your robot.' ) }) it('should render both buttons', () => { diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 1bed99157be8..db042a2ce65e 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -21,7 +21,6 @@ import { useRunCommands, } from '../ProtocolUpload/hooks' import { useNotifyRunQuery } from '../../resources/runs' -import { useFeatureFlag } from '../../redux/config' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import type { UseQueryOptions } from 'react-query' @@ -82,7 +81,7 @@ export function useRunStatus( !([ RUN_STATUS_FAILED, RUN_STATUS_SUCCEEDED, - RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_STOPPED, ] as RunStatus[]).includes(lastRunStatus.current), onSuccess: data => (lastRunStatus.current = data?.data?.status ?? null), ...options, @@ -188,11 +187,6 @@ export function useRunErrors(runId: string | null): RunData['errors'] { export function useProtocolHasRunTimeParameters(runId: string | null): boolean { const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const runTimeParametersFF = useFeatureFlag('enableRunTimeParameters') - - console.log( - 'TODO: delete the feature flag logic', - mostRecentAnalysis?.runTimeParameters - ) - return runTimeParametersFF + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + return runTimeParameters.length > 0 } diff --git a/app/src/organisms/TakeoverModal/TakeoverModal.tsx b/app/src/organisms/TakeoverModal/TakeoverModal.tsx index 3dab071bdec7..c87f33fc1500 100644 --- a/app/src/organisms/TakeoverModal/TakeoverModal.tsx +++ b/app/src/organisms/TakeoverModal/TakeoverModal.tsx @@ -32,7 +32,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { confirmTerminate, terminateInProgress, } = props - const { i18n, t } = useTranslation('shared') + const { i18n, t } = useTranslation(['shared', 'branded']) const terminateHeader: ModalHeaderBaseProps = { title: t('terminate') + '?', @@ -46,7 +46,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { - {t('confirm_terminate')} + {t('branded:confirm_terminate')} - {t('computer_in_app_is_controlling_robot')} + {t('branded:computer_in_app_is_controlling_robot')} ) : null} {(downloading || downloaded) && error == null ? ( - + closeModal(true)} closeOnOutsideClick={true} footer={appUpdateFooter} @@ -191,7 +194,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { > - {t('update_requires_restarting')} + {t('branded:update_requires_restarting_app')} diff --git a/app/src/organisms/UpdateRobotBanner/index.tsx b/app/src/organisms/UpdateRobotBanner/index.tsx index ced443a2018c..86e2201bf841 100644 --- a/app/src/organisms/UpdateRobotBanner/index.tsx +++ b/app/src/organisms/UpdateRobotBanner/index.tsx @@ -25,7 +25,7 @@ export function UpdateRobotBanner( props: UpdateRobotBannerProps ): JSX.Element | null { const { robot, ...styleProps } = props - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) const { autoUpdateAction } = useSelector((state: State) => { return getRobotUpdateDisplayInfo(state, robot?.name) @@ -40,7 +40,7 @@ export function UpdateRobotBanner( > - {t('robot_software_update_required')} + {t('branded:robot_software_update_required')} handleUpdateBuildroot(robot)} diff --git a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx index 60ff6cc18de1..7d625254a2f8 100644 --- a/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx +++ b/app/src/organisms/UpdateRobotSoftware/UpdateSoftware.tsx @@ -4,25 +4,21 @@ import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, BORDERS, - Box, COLORS, DIRECTION_COLUMN, Flex, + Icon, JUSTIFY_CENTER, SPACING, StyledText, TYPOGRAPHY, } from '@opentrons/components' -import { ProgressBar } from '../../atoms/ProgressBar' - interface UpdateSoftwareProps { updateType: 'downloading' | 'validating' | 'sendingFile' | 'installing' | null - processProgress: number } export function UpdateSoftware({ updateType, - processProgress, }: UpdateSoftwareProps): JSX.Element { const { t } = useTranslation('device_settings') const renderText = (): string | null => { @@ -52,6 +48,13 @@ export function UpdateSoftware({ height="33rem" borderRadius={BORDERS.borderRadius12} > + - - - ) } diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx index 242b40c4be80..5db3c1358eb4 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateRobotSoftware.test.tsx @@ -113,7 +113,7 @@ describe('UpdateRobotSoftware', () => { render() expect(mockBeforeCommitting).toBeCalled() expect(UpdateSoftware).toBeCalledWith( - { updateType: 'installing', processProgress: 0 }, + { updateType: 'installing' }, expect.anything() ) screen.getByText('mock UpdateSoftware') diff --git a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx index 913f2c26deae..680de1b01472 100644 --- a/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx +++ b/app/src/organisms/UpdateRobotSoftware/__tests__/UpdateSoftware.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react' import { screen } from '@testing-library/react' -import { describe, it, beforeEach, expect } from 'vitest' +import { describe, it, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' -import { COLORS } from '@opentrons/components' import { i18n } from '../../../i18n' import { UpdateSoftware } from '../UpdateSoftware' @@ -18,47 +17,34 @@ describe('UpdateSoftware', () => { beforeEach(() => { props = { updateType: 'downloading', - processProgress: 50, } }) - it('should render text and progressbar - downloading software', () => { + it('should render text - downloading software', () => { render(props) screen.getByText('Downloading software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle(`background: ${String(COLORS.blue50)}`) - expect(bar).toHaveStyle('width: 50%') }) - it('should render text and progressbar - sending software', () => { + it('should render text - sending software', () => { props = { ...props, - processProgress: 20, updateType: 'sendingFile', } render(props) screen.getByText('Sending software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 20%') }) - it('should render text and progressbar - validating software', () => { + it('should render text - validating software', () => { props = { ...props, - processProgress: 80, updateType: 'validating', } render(props) screen.getByText('Validating software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 80%') }) - it('should render text and progressbar - installing software', () => { + it('should render text - installing software', () => { props = { ...props, - processProgress: 5, updateType: 'installing', } render(props) screen.getByText('Installing software...') - const bar = screen.getByTestId('ProgressBar_Bar') - expect(bar).toHaveStyle('width: 5%') }) }) diff --git a/app/src/organisms/UpdateRobotSoftware/index.tsx b/app/src/organisms/UpdateRobotSoftware/index.tsx index c88f31974915..4d61272ac6f8 100644 --- a/app/src/organisms/UpdateRobotSoftware/index.tsx +++ b/app/src/organisms/UpdateRobotSoftware/index.tsx @@ -37,7 +37,7 @@ export function UpdateRobotSoftware( const dispatch = useDispatch() const session = useSelector(getRobotUpdateSession) - const { step, stage, progress, error: sessionError } = session ?? { + const { step, stage, error: sessionError } = session ?? { step: null, error: null, } @@ -76,11 +76,6 @@ export function UpdateRobotSoftware( beforeCommittingSuccessfulUpdate && beforeCommittingSuccessfulUpdate() } } - return ( - - ) + return } } diff --git a/app/src/pages/AppSettings/GeneralSettings.tsx b/app/src/pages/AppSettings/GeneralSettings.tsx index 99bdf464d040..553f0e563561 100644 --- a/app/src/pages/AppSettings/GeneralSettings.tsx +++ b/app/src/pages/AppSettings/GeneralSettings.tsx @@ -54,7 +54,7 @@ const GITHUB_LINK = const ENABLE_APP_UPDATE_NOTIFICATIONS = 'Enable app update notifications' export function GeneralSettings(): JSX.Element { - const { t } = useTranslation(['app_settings', 'shared']) + const { t } = useTranslation(['app_settings', 'shared', 'branded']) const dispatch = useDispatch() const trackEvent = useTrackEvent() const [ @@ -113,7 +113,7 @@ export function GeneralSettings(): JSX.Element { type="warning" onCloseClick={() => setShowUpdateBanner(false)} > - {t('opentrons_app_update_available_variation')} + {t('branded:opentrons_app_update_available_variation')} - {t('versions_sync')} + {t('branded:versions_sync')} @@ -218,7 +218,7 @@ export function GeneralSettings(): JSX.Element { alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN} > - {t('receive_alert')} + {t('branded:receive_alert')} () const analyticsOptedIn = useSelector((s: State) => getAnalyticsOptedIn(s)) diff --git a/app/src/pages/ConnectViaUSB/index.tsx b/app/src/pages/ConnectViaUSB/index.tsx index 961da9b60929..72130c5444cb 100644 --- a/app/src/pages/ConnectViaUSB/index.tsx +++ b/app/src/pages/ConnectViaUSB/index.tsx @@ -22,7 +22,7 @@ import { StepMeter } from '../../atoms/StepMeter' import { MediumButton } from '../../atoms/buttons' export function ConnectViaUSB(): JSX.Element { - const { i18n, t } = useTranslation(['device_settings', 'shared']) + const { i18n, t } = useTranslation(['device_settings', 'shared', 'branded']) const history = useHistory() // TODO(bh, 2023-5-31): active connections from /system/connected isn't exactly the right way to monitor for a usb connection - // the system-server tracks active connections by authorization token, which is valid for 2 hours @@ -92,7 +92,7 @@ export function ConnectViaUSB(): JSX.Element { color={COLORS.grey60} textAlign={TYPOGRAPHY.textAlignCenter} > - {t('find_your_robot')} + {t('branded:find_your_robot')} @@ -134,7 +134,7 @@ export function ConnectViaUSB(): JSX.Element { {t('connect_via_usb_description_2')} - {t('connect_via_usb_description_3')} + {t('branded:connect_via_usb_description_3')}
diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index d2e5b5083251..27d0d83a25c0 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -19,6 +19,10 @@ import { SINGLE_RIGHT_CUTOUTS, SINGLE_LEFT_SLOT_FIXTURE, SINGLE_RIGHT_SLOT_FIXTURE, + SINGLE_LEFT_CUTOUTS, + SINGLE_CENTER_SLOT_FIXTURE, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' @@ -28,7 +32,11 @@ import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetails import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { getTopPortalEl } from '../../App/portal' -import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckConfiguration, +} from '@opentrons/shared-data' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ @@ -53,6 +61,7 @@ export function DeckConfigurationEditor(): JSX.Element { setShowDiscardChangeModal, ] = React.useState(false) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() @@ -66,19 +75,53 @@ export function DeckConfigurationEditor(): JSX.Element { setShowConfigurationModal(true) } - const handleClickRemove = (cutoutId: CutoutId): void => { - setCurrentDeckConfig(prevDeckConfig => - prevDeckConfig.map(fixture => - fixture.cutoutId === cutoutId + const handleClickRemove = ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ): void => { + let replacementFixtureId: CutoutFixtureId = SINGLE_CENTER_SLOT_FIXTURE + if (SINGLE_RIGHT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_RIGHT_SLOT_FIXTURE + } else if (SINGLE_LEFT_CUTOUTS.includes(cutoutId)) { + replacementFixtureId = SINGLE_LEFT_SLOT_FIXTURE + } + + const fixtureGroup = + deckDef.cutoutFixtures.find(cf => cf.id === cutoutFixtureId) + ?.fixtureGroup ?? {} + + let newDeckConfig = deckConfig + if (cutoutId in fixtureGroup) { + const groupMap = + fixtureGroup[cutoutId]?.find(group => + Object.entries(group).every(([cId, cfId]) => + deckConfig.find( + config => + config.cutoutId === cId && config.cutoutFixtureId === cfId + ) + ) + ) ?? {} + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId in groupMap ? { - ...fixture, - cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId) - ? SINGLE_RIGHT_SLOT_FIXTURE - : SINGLE_LEFT_SLOT_FIXTURE, + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, } - : fixture + : cutoutConfig ) - ) + } else { + newDeckConfig = deckConfig.map(cutoutConfig => + cutoutConfig.cutoutId === cutoutId + ? { + ...cutoutConfig, + cutoutFixtureId: replacementFixtureId, + opentronsModuleSerialNumber: undefined, + } + : cutoutConfig + ) + } + updateDeckConfiguration(newDeckConfig) } const handleClickConfirm = (): void => { diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx index 8c36199315fc..672c2231e311 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx @@ -37,7 +37,6 @@ import { useCurrentRunId } from '../../../organisms/ProtocolUpload/hooks' import { OPENTRONS_USB } from '../../../redux/discovery' import { fetchProtocols } from '../../../redux/protocol-storage' import { appShellRequestor } from '../../../redux/shell/remote' -import { useFeatureFlag } from '../../../redux/config' import type { DesktopRouteParams, @@ -180,7 +179,7 @@ function PageContents(props: PageContentsProps): JSX.Element { const protocolRunHeaderRef = React.useRef(null) const listRef = React.useRef(null) const [jumpedIndex, setJumpedIndex] = React.useState(null) - const enableRunTimeParameters = useFeatureFlag('enableRunTimeParameters') + React.useEffect(() => { if (jumpedIndex != null) { setTimeout(() => setJumpedIndex(null), JUMPED_STEP_HIGHLIGHT_DELAY_MS) @@ -236,9 +235,7 @@ function PageContents(props: PageContentsProps): JSX.Element { /> - {enableRunTimeParameters ? ( - - ) : null} + diff --git a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx b/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx index 940c7694c540..a7e9076bb636 100644 --- a/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx +++ b/app/src/pages/InitialLoadingScreen/__tests__/InitialLoadingScreen.test.tsx @@ -2,33 +2,31 @@ import * as React from 'react' import { vi, it, describe, beforeEach, afterEach } from 'vitest' import { screen } from '@testing-library/react' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' + import { renderWithProviders } from '../../../__testing-utils__' -import { getOnDeviceDisplaySettings } from '../../../redux/config' import { getIsShellReady } from '../../../redux/shell' import { InitialLoadingScreen } from '..' -import type { OnDeviceDisplaySettings } from '../../../redux/config/schema-types' +import type { UseQueryResult } from 'react-query' +import type { RobotSettingsResponse } from '@opentrons/api-client' +vi.mock('@opentrons/react-api-client') vi.mock('../../../redux/config') vi.mock('../../../redux/shell') -const mockSettings = { - sleepMs: 60 * 1000 * 60 * 24 * 7, - brightness: 4, - textSize: 1, - unfinishedUnboxingFlowRoute: null, -} as OnDeviceDisplaySettings - const render = () => { return renderWithProviders() } describe('InitialLoadingScreen', () => { beforeEach(() => { - vi.mocked(getOnDeviceDisplaySettings).mockReturnValue(mockSettings) vi.mocked(getIsShellReady).mockReturnValue(false) + vi.mocked(useRobotSettingsQuery).mockReturnValue(({ + data: { settings: [] }, + } as unknown) as UseQueryResult) }) afterEach(() => { diff --git a/app/src/pages/InitialLoadingScreen/index.tsx b/app/src/pages/InitialLoadingScreen/index.tsx index 5171b2720b3e..d57519bfa3b0 100644 --- a/app/src/pages/InitialLoadingScreen/index.tsx +++ b/app/src/pages/InitialLoadingScreen/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { Redirect } from 'react-router-dom' import { useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -10,30 +9,23 @@ import { JUSTIFY_CENTER, SPACING, } from '@opentrons/components' -import { getOnDeviceDisplaySettings } from '../../redux/config' +import { useRobotSettingsQuery } from '@opentrons/react-api-client' import { getIsShellReady } from '../../redux/shell' -const getTargetPath = ( - isShellReady: boolean, - unfinishedUnboxingFlowRoute: string | null -): string | null => { - if (!isShellReady) { - return null - } - if (unfinishedUnboxingFlowRoute != null) { - return unfinishedUnboxingFlowRoute - } - - return '/dashboard' -} -export function InitialLoadingScreen(): JSX.Element { - const { unfinishedUnboxingFlowRoute } = useSelector( - getOnDeviceDisplaySettings - ) +export function InitialLoadingScreen({ + children, +}: { + children?: React.ReactNode +}): JSX.Element { const isShellReady = useSelector(getIsShellReady) - const targetPath = getTargetPath(isShellReady, unfinishedUnboxingFlowRoute) - return ( + // ensure robot-server api is up and settings query data available for localization provider + const { settings } = + useRobotSettingsQuery({ retry: true, retryDelay: 1000 }).data ?? {} + + return isShellReady && settings != null ? ( + <>{children} + ) : ( - {targetPath != null && } ) } diff --git a/app/src/pages/Labware/hooks.tsx b/app/src/pages/Labware/hooks.tsx index caf37544be54..b1453738652d 100644 --- a/app/src/pages/Labware/hooks.tsx +++ b/app/src/pages/Labware/hooks.tsx @@ -69,7 +69,7 @@ export function useLabwareFailure(): { labwareFailureMessage: string | null clearLabwareFailure: () => unknown } { - const { t } = useTranslation('labware_landing') + const { t } = useTranslation(['labware_landing', 'branded']) const dispatch = useDispatch() const labwareFailure = useSelector(getAddLabwareFailure) @@ -82,7 +82,7 @@ export function useLabwareFailure(): { } else if (failedFile?.type === 'DUPLICATE_LABWARE_FILE') { errorMessage = t('duplicate_labware_def') } else if (failedFile?.type === 'OPENTRONS_LABWARE_FILE') { - errorMessage = t('opentrons_labware_def') + errorMessage = t('branded:opentrons_labware_def') } labwareFailureMessage = failedFile != null diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 1bbf40992349..3823525ccb4e 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -18,8 +18,8 @@ import { POSITION_FIXED, POSITION_RELATIVE, SPACING, - TYPOGRAPHY, StyledText, + TYPOGRAPHY, } from '@opentrons/components' import { useUpdateRobotNameMutation } from '@opentrons/react-api-client' @@ -121,7 +121,7 @@ export function NameRobot(): JSX.Element { defaultValues: { newRobotName: '', }, - resolver: resolver, + resolver, }) const newRobotName = watch('newRobotName') @@ -298,7 +298,7 @@ export function NameRobot(): JSX.Element { { field.onChange(input) - trigger('newRobotName') + void trigger('newRobotName') }} keyboardRef={keyboardRef} /> diff --git a/app/src/pages/NetworkSetupMenu/index.tsx b/app/src/pages/NetworkSetupMenu/index.tsx index 7250eaa3dda6..11909bdb77ff 100644 --- a/app/src/pages/NetworkSetupMenu/index.tsx +++ b/app/src/pages/NetworkSetupMenu/index.tsx @@ -34,13 +34,13 @@ const NetworkSetupOptions = [ { title: 'usb', iconName: 'usb' as IconName, - description: 'connection_description_usb', + description: 'branded:connection_description_usb', destinationPath: '/network-setup/usb', }, ] export function NetworkSetupMenu(): JSX.Element { - const { t } = useTranslation('device_settings') + const { t } = useTranslation(['device_settings', 'branded']) return ( <> @@ -73,7 +73,7 @@ export function NetworkSetupMenu(): JSX.Element { color={COLORS.grey60} textAlign={TYPOGRAPHY.textAlignCenter} > - {t('network_setup_menu_description')} + {t('branded:network_setup_menu_description')}
- {t('send_a_protocol_to_store')} + {t('branded:send_a_protocol_to_store')} ) diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index 1ed35b8632f9..305ca99c7bcf 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -60,7 +60,7 @@ export function ProtocolCard(props: { showFailedAnalysisModal, setShowFailedAnalysisModal, ] = React.useState(false) - const { t, i18n } = useTranslation('protocol_info') + const { t, i18n } = useTranslation(['protocol_info', 'branded']) const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const longpress = useLongPress() const queryClient = useQueryClient() @@ -264,7 +264,9 @@ export function ProtocolCard(props: { }} /> - {t('delete_protocol_from_app')} + + {t('branded:delete_protocol_from_app')} +
() const [navMenuIsOpened, setNavMenuIsOpened] = React.useState(false) @@ -57,6 +60,11 @@ export function ProtocolDashboard(): JSX.Element { const pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinnedProtocols: ProtocolResource[] = [] + // TODO(sb, 4/15/24): The quick transfer button is going to be moved to a new quick transfer + // tab before the feature is released. Because of this, we're not adding test cov + // for this button in ProtocolDashboard + const enableQuickTransferFF = useFeatureFlag('enableQuickTransfer') + // We only need to grab out the pinned protocol data once all the protocols load // and if we have pinned ids stored in config. if (protocolsData.length > 0 && pinnedProtocolIds.length > 0) { @@ -272,6 +280,15 @@ export function ProtocolDashboard(): JSX.Element { ) : null} + {enableQuickTransferFF && ( + { + history.push('/quick-transfer') + }} + /> + )} ) } diff --git a/app/src/pages/ProtocolDetails/Parameters.tsx b/app/src/pages/ProtocolDetails/Parameters.tsx index 0e12e8d7997d..0b280a2af3df 100644 --- a/app/src/pages/ProtocolDetails/Parameters.tsx +++ b/app/src/pages/ProtocolDetails/Parameters.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, +} from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -59,23 +63,22 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { makeSnackbar(t('start_setup_customize_values')) } - const getRange = (parameter: RunTimeParameter): string => { + const formatRange = (parameter: RunTimeParameter): string => { const { type } = parameter - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 const numChoices = 'choices' in parameter ? parameter.choices.length : 0 + const minMax = formatRunTimeParameterMinMax(parameter) let range: string | null = null if (numChoices === 2 && 'choices' in parameter) { - range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` + range = orderRuntimeParameterRangeOptions(parameter.choices) } switch (type) { - case 'boolean': { + case 'bool': { return t('on_off') } case 'float': case 'int': { - return `${min}-${max}` + return minMax } case 'str': { return range ?? t('num_choices', { num: numChoices }) @@ -118,12 +121,12 @@ export const Parameters = (props: { protocolId: string }): JSX.Element => { - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} - {getRange(parameter)} + {formatRange(parameter)} diff --git a/app/src/pages/ProtocolDetails/fixtures.ts b/app/src/pages/ProtocolDetails/fixtures.ts index 4f5cfa6cdada..dd23bc4623e3 100644 --- a/app/src/pages/ProtocolDetails/fixtures.ts +++ b/app/src/pages/ProtocolDetails/fixtures.ts @@ -5,7 +5,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, value: false, }, @@ -13,15 +13,15 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', - default: true, + type: 'bool', + default: false, value: true, }, { displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -29,7 +29,7 @@ export const mockRunTimeParameterData: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, value: true, }, diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index e44e3f7015b6..850fd0a80168 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -44,7 +44,6 @@ import { getApplyHistoricOffsets, getPinnedProtocolIds, updateConfigValue, - useFeatureFlag, } from '../../redux/config' import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { @@ -189,10 +188,8 @@ const ProtocolSectionTabs = ({ currentOption, setCurrentOption, }: ProtocolSectionTabsProps): JSX.Element => { - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') - const options = enableRtpFF - ? protocolSectionTabOptions - : protocolSectionTabOptionsWithoutParameters + const options = protocolSectionTabOptions + return ( {options.map(option => { @@ -308,7 +305,6 @@ export function ProtocolDetails(): JSX.Element | null { 'protocol_info', 'shared', ]) - const enableRtpFF = useFeatureFlag('enableRunTimeParameters') const { protocolId } = useParams() const { missingProtocolHardware, @@ -326,9 +322,7 @@ export function ProtocolDetails(): JSX.Element | null { const [showParameters, setShowParameters] = React.useState(false) const queryClient = useQueryClient() const [currentOption, setCurrentOption] = React.useState( - enableRtpFF - ? protocolSectionTabOptions[0] - : protocolSectionTabOptionsWithoutParameters[0] + protocolSectionTabOptions[0] ) const [showMaxPinsAlert, setShowMaxPinsAlert] = React.useState(false) @@ -352,13 +346,12 @@ export function ProtocolDetails(): JSX.Element | null { let pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? [] const pinned = pinnedProtocolIds.includes(protocolId) - const { data: protocolData } = useProtocolQuery(protocolId) const { data: mostRecentAnalysis, } = useProtocolAnalysisAsDocumentQuery( protocolId, - last(protocolData?.data.analysisSummaries)?.id ?? null, - { enabled: protocolData != null } + last(protocolRecord?.data.analysisSummaries)?.id ?? null, + { enabled: protocolRecord != null } ) const shouldApplyOffsets = useSelector(getApplyHistoricOffsets) diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index b4b5af7e0c82..030e3c1a9a06 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -20,7 +20,7 @@ import { getDeckDefFromRobotType, FLEX_ROBOT_TYPE, STAGING_AREA_RIGHT_SLOT_FIXTURE, - flexDeckDefV4, + flexDeckDefV5, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' @@ -229,14 +229,14 @@ describe('ProtocolSetup', () => { .calledWith(RUN_ID) .thenReturn(CREATED_AT) when(vi.mocked(getProtocolModulesInfo)) - .calledWith(mockEmptyAnalysis, flexDeckDefV4 as any) + .calledWith(mockEmptyAnalysis, flexDeckDefV5 as any) .thenReturn([]) when(vi.mocked(getUnmatchedModulesForProtocol)) .calledWith([], []) .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] }) when(vi.mocked(getDeckDefFromRobotType)) .calledWith('OT-3 Standard') - .thenReturn(flexDeckDefV4 as any) + .thenReturn(flexDeckDefV5 as any) when(vi.mocked(useNotifyRunQuery)) .calledWith(RUN_ID, { staleTime: Infinity }) .thenReturn({ @@ -320,7 +320,7 @@ describe('ProtocolSetup', () => { data: mockRobotSideAnalysis, } as any) when(vi.mocked(getProtocolModulesInfo)) - .calledWith(mockRobotSideAnalysis, flexDeckDefV4 as any) + .calledWith(mockRobotSideAnalysis, flexDeckDefV5 as any) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) .calledWith([], mockProtocolModuleInfo) @@ -337,7 +337,7 @@ describe('ProtocolSetup', () => { when(vi.mocked(getProtocolModulesInfo)) .calledWith( { ...mockRobotSideAnalysis, liquids: mockLiquids }, - flexDeckDefV4 as any + flexDeckDefV5 as any ) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) @@ -364,7 +364,7 @@ describe('ProtocolSetup', () => { ...mockRobotSideAnalysis, runTimeParameters: mockRunTimeParameterData, }, - flexDeckDefV4 as any + flexDeckDefV5 as any ) .thenReturn(mockProtocolModuleInfo) when(vi.mocked(getUnmatchedModulesForProtocol)) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index be90fcfa80ed..14b871f839cc 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -69,7 +69,6 @@ import { getProtocolUsesGripper, } from '../../organisms/ProtocolSetupInstruments/utils' import { - useProtocolHasRunTimeParameters, useRunControls, useRunStatus, } from '../../organisms/RunTimeControl/hooks' @@ -82,7 +81,7 @@ import { ANALYTICS_PROTOCOL_RUN_START, useTrackEvent, } from '../../redux/analytics' -import { getIsHeaterShakerAttached, useFeatureFlag } from '../../redux/config' +import { getIsHeaterShakerAttached } from '../../redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' @@ -257,9 +256,6 @@ function PrepareToRun({ const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const history = useHistory() const { makeSnackbar } = useToaster() - const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') - const hasRunTimeParameters = useProtocolHasRunTimeParameters(runId) - // Watch for scrolling to toggle dropshadow const scrollRef = React.useRef(null) const [isScrolled, setIsScrolled] = React.useState(false) const observer = new IntersectionObserver(([entry]) => { @@ -366,6 +362,12 @@ function PrepareToRun({ }) const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? [] + const hasRunTimeParameters = runTimeParameters.length > 0 + const hasCustomRunTimeParameters = runTimeParameters.some( + parameter => parameter.value !== parameter.default + ) + const [ showConfirmCancelModal, setShowConfirmCancelModal, @@ -623,11 +625,11 @@ function PrepareToRun({ doorStatus?.data.status === 'open' && doorStatus?.data.doorRequiredClosedForProtocol - // TODO(Jr, 3/20/24): wire up custom values - const hasCustomValues = false - const parametersDetail = hasCustomValues - ? t('custom_values') - : t('default_values') + const parametersDetail = hasRunTimeParameters + ? hasCustomRunTimeParameters + ? t('custom_values') + : t('default_values') + : t('no_parameters_specified') return ( <> @@ -730,20 +732,14 @@ function PrepareToRun({ disabled={lpcDisabledReason != null} disabledReason={lpcDisabledReason} /> - {enableRunTimeParametersFF ? ( - setSetupScreen('view only parameters')} - title={t('parameters')} - detail={t( - hasRunTimeParameters - ? parametersDetail - : t('no_parameters_specified') - )} - subDetail={null} - status="general" - disabled={!hasRunTimeParameters} - /> - ) : null} + setSetupScreen('view only parameters')} + title={t('parameters')} + detail={parametersDetail} + subDetail={null} + status="general" + disabled={!hasRunTimeParameters} + /> setSetupScreen('labware')} title={t('labware')} diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx index ce09a610ff7a..7827c82175fb 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx @@ -38,28 +38,28 @@ const mockRTPData = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'a dry run description', - type: 'boolean', + type: 'bool', default: false, }, { displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: '', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Trash Tips', variableName: 'TIP_TRASH', description: 'throw tip in trash', - type: 'boolean', + type: 'bool', default: true, }, { displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature?', - type: 'boolean', + type: 'bool', default: true, }, { @@ -261,7 +261,7 @@ describe('useRequiredProtocolLabware', () => { }) }) -describe('useMissingProtocolHardware', () => { +describe.only('useMissingProtocolHardware', () => { let wrapper: React.FunctionComponent<{ children: React.ReactNode }> beforeEach(() => { vi.mocked(useInstrumentsQuery).mockReturnValue({ @@ -343,14 +343,6 @@ describe('useMissingProtocolHardware', () => { connected: false, hasSlotConflict: true, }, - { - hardwareType: 'fixture', - cutoutFixtureId: 'singleRightSlot', - location: { - cutout: 'cutoutD3', - }, - hasSlotConflict: true, - }, ], conflictedSlots: ['D3'], }) @@ -374,6 +366,21 @@ describe('useMissingProtocolHardware', () => { data: { data: [mockHeaterShaker] }, isLoading: false, } as any) + vi.mocked(useDeckConfigurationQuery).mockReturnValue({ + data: [ + omitBy( + FLEX_SIMPLEST_DECK_CONFIG, + ({ cutoutId }) => cutoutId === 'cutoutD3' + ), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'heaterShakerModuleV1', + opentronsModuleSerialNumber: mockHeaterShaker.serialNumber, + }, + ], + isLoading: false, + } as any) + const { result } = renderHook( () => useMissingProtocolHardware(PROTOCOL_ANALYSIS.id), { wrapper } @@ -384,7 +391,7 @@ describe('useMissingProtocolHardware', () => { conflictedSlots: [], }) }) - it('should return conflicting slot when module location is configured with something other than single slot fixture', () => { + it('should return conflicting slot when module location is configured with something other than module fixture', () => { vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [ @@ -425,11 +432,10 @@ describe('useMissingProtocolHardware', () => { expect(result.current).toEqual({ missingProtocolHardware: [ { - hardwareType: 'fixture', - cutoutFixtureId: 'singleRightSlot', - location: { - cutout: 'cutoutD3', - }, + hardwareType: 'module', + moduleModel: 'heaterShakerModuleV1', + slot: 'D3', + connected: false, hasSlotConflict: true, }, ], diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 9931a49444ff..22e049f4ca89 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -9,10 +9,12 @@ import { import { FLEX_ROBOT_TYPE, FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, - SINGLE_SLOT_FIXTURES, getCutoutIdForSlotName, getDeckDefFromRobotType, RunTimeParameter, + getCutoutFixtureIdsForModuleModel, + getCutoutFixturesForModuleModel, + FLEX_MODULE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../utils' import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' @@ -28,7 +30,6 @@ import type { RobotType, } from '@opentrons/shared-data' import type { LabwareSetupItem } from '../utils' -import type { AttachedModule } from '@opentrons/api-client' interface ProtocolPipette { hardwareType: 'pipette' @@ -105,29 +106,38 @@ export const useRequiredProtocolHardwareFromAnalysis = ( ] : [] - const handleModuleConnectionCheckFor = ( - attachedModules: AttachedModule[], - model: ModuleModel - ): boolean => { - const ASSUME_ALWAYS_CONNECTED_MODULES = ['magneticBlockV1'] - - return !ASSUME_ALWAYS_CONNECTED_MODULES.includes(model) - ? attachedModules.some(m => m.moduleModel === model) - : true - } - const requiredModules: ProtocolModule[] = analysis.modules.map( ({ location, model }) => { + const cutoutIdForSlotName = getCutoutIdForSlotName( + location.slotName, + deckDef + ) + const moduleFixtures = getCutoutFixturesForModuleModel(model, deckDef) + + const configuredModuleSerialNumber = + deckConfig.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === cutoutIdForSlotName && + moduleFixtures.map(mf => mf.id).includes(cutoutFixtureId) + )?.opentronsModuleSerialNumber ?? null + const isConnected = moduleFixtures.every( + mf => mf.expectOpentronsModuleSerialNumber + ) + ? attachedModules.some( + m => + m.moduleModel === model && + m.serialNumber === configuredModuleSerialNumber + ) + : true return { hardwareType: 'module', moduleModel: model, slot: location.slotName, - connected: handleModuleConnectionCheckFor(attachedModules, model), + connected: isConnected, hasSlotConflict: deckConfig.some( ({ cutoutId, cutoutFixtureId }) => cutoutId === getCutoutIdForSlotName(location.slotName, deckDef) && - cutoutFixtureId != null && - !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + cutoutFixtureId !== getCutoutFixtureIdsForModuleModel(model)[0] ), } } @@ -161,16 +171,22 @@ export const useRequiredProtocolHardwareFromAnalysis = ( } ) - const requiredFixtures = requiredDeckConfigCompatibility.map( - ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + const requiredFixtures = requiredDeckConfigCompatibility + // filter out all module fixtures as they're handled in the requiredModules section via hardwareType === 'module' + .filter( + ({ requiredAddressableAreas }) => + !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => + requiredAddressableAreas.includes(modAA) + ) + ) + .map(({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ hardwareType: 'fixture' as const, cutoutFixtureId: compatibleCutoutFixtureIds[0], location: { cutout: cutoutId }, hasSlotConflict: cutoutFixtureId != null && !compatibleCutoutFixtureIds.includes(cutoutFixtureId), - }) - ) + })) return { requiredProtocolHardware: [ @@ -200,150 +216,7 @@ export const useRunTimeParameters = ( { enabled: protocolData != null } ) - const mockData: RunTimeParameter[] = [ - { - value: false, - displayName: 'Dry Run', - variableName: 'DRYRUN', - description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', - default: false, - }, - { - value: true, - displayName: 'Use Gripper', - variableName: 'USE_GRIPPER', - description: 'For using the gripper.', - type: 'boolean', - default: true, - }, - { - value: true, - displayName: 'Trash Tips', - variableName: 'TIP_TRASH', - description: - 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', - default: true, - }, - { - value: true, - displayName: 'Deactivate Temperatures', - variableName: 'DEACTIVATE_TEMP', - description: 'deactivate temperature on the module', - type: 'boolean', - default: true, - }, - { - value: 4, - displayName: 'Columns of Samples', - variableName: 'COLUMNS', - description: 'How many columns do you want?', - type: 'int', - min: 1, - max: 14, - default: 4, - }, - { - value: 6, - displayName: 'PCR Cycles', - variableName: 'PCR_CYCLES', - description: 'number of PCR cycles on a thermocycler', - type: 'int', - min: 1, - max: 10, - default: 6, - }, - { - value: 6.5, - displayName: 'EtoH Volume', - variableName: 'ETOH_VOLUME', - description: '70% ethanol volume', - type: 'float', - suffix: 'mL', - min: 1.5, - max: 10.0, - default: 6.5, - }, - { - value: 'none', - displayName: 'Default Module Offsets', - variableName: 'DEFAULT_OFFSETS', - description: 'default module offsets for temp, H-S, and none', - type: 'str', - choices: [ - { - displayName: 'No offsets', - value: 'none', - }, - { - displayName: 'temp offset', - value: '1', - }, - { - displayName: 'heater-shaker offset', - value: '2', - }, - ], - default: 'none', - }, - { - value: 'left', - displayName: 'pipette mount', - variableName: 'mont', - description: 'pipette mount', - type: 'str', - choices: [ - { - displayName: 'Left', - value: 'left', - }, - { - displayName: 'Right', - value: 'right', - }, - ], - default: 'left', - }, - { - value: 'flex', - displayName: 'short test case', - variableName: 'short 2 options', - description: 'this play 2 short options', - type: 'str', - choices: [ - { - displayName: 'OT-2', - value: 'ot2', - }, - { - displayName: 'Flex', - value: 'flex', - }, - ], - default: 'flex', - }, - { - value: 'flex', - displayName: 'long test case', - variableName: 'long 2 options', - description: 'this play 2 long options', - type: 'str', - choices: [ - { - displayName: 'I am kind of long text version', - value: 'ot2', - }, - { - displayName: 'I am kind of long text version. Today is 3/15', - value: 'flex', - }, - ], - default: 'flex', - }, - ] - // TODO(jr, 3/14/24): remove the mockData - return analysis?.runTimeParameters ?? mockData + return analysis?.runTimeParameters ?? [] } /** @@ -421,9 +294,16 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = ( ), ...deckConfigCompatibility .filter( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => + ({ + cutoutFixtureId, + compatibleCutoutFixtureIds, + requiredAddressableAreas, + }) => cutoutFixtureId != null && - !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) + !compatibleCutoutFixtureIds.some(id => id === cutoutFixtureId) && + !FLEX_MODULE_ADDRESSABLE_AREAS.some(modAA => + requiredAddressableAreas.includes(modAA) + ) // modules are already included via requiredProtocolHardware ) .map(({ compatibleCutoutFixtureIds, cutoutId }) => ({ hardwareType: 'fixture' as const, diff --git a/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx b/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx index e7772654a383..4073a356aeeb 100644 --- a/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx +++ b/app/src/pages/RobotDashboard/AnalyticsOptInModal.tsx @@ -28,7 +28,7 @@ interface AnalyticsOptInModalProps { export function AnalyticsOptInModal({ setShowAnalyticsOptInModal, }: AnalyticsOptInModalProps): JSX.Element { - const { t } = useTranslation(['app_settings', 'shared']) + const { t } = useTranslation(['app_settings', 'shared', 'branded']) const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) @@ -57,7 +57,7 @@ export function AnalyticsOptInModal({ } return ( - + () const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -144,7 +148,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { setCurrentOption('Privacy')} iconName="privacy" /> diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index 32f87a8047c9..2b43991a88fc 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -7,6 +7,7 @@ import { RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_IDLE, RUN_STATUS_STOP_REQUESTED, + RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' import { useAllCommandsQuery, @@ -30,12 +31,14 @@ import { getLocalRobot } from '../../../redux/discovery' import { CancelingRunModal } from '../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { RunPausedSplash } from '../../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash' import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' import { useNotifyLastRunCommandKey, useNotifyRunQuery, } from '../../../resources/runs' +import { useFeatureFlag } from '../../../redux/config' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses } from '@opentrons/api-client' @@ -47,12 +50,15 @@ vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock( '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) +vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash') vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') vi.mock('../../../organisms/OpenDoorAlertModal') vi.mock('../../../resources/runs') +vi.mock('../../../redux/config') + const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' const PROTOCOL_ID = 'protocol_id' @@ -85,6 +91,7 @@ describe('RunningProtocol', () => { data: { id: RUN_ID, protocolId: PROTOCOL_ID, + errors: [], }, }, } as any) @@ -133,6 +140,9 @@ describe('RunningProtocol', () => { vi.mocked(useNotifyLastRunCommandKey).mockReturnValue({ data: {}, } as any) + when(vi.mocked(useFeatureFlag)) + .calledWith('enableRunNotes') + .thenReturn(true) }) afterEach(() => { @@ -166,6 +176,14 @@ describe('RunningProtocol', () => { expect(vi.mocked(OpenDoorAlertModal)).toHaveBeenCalled() }) + it(`should display a Run Paused splash screen if the run status is "${RUN_STATUS_AWAITING_RECOVERY}"`, () => { + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID, { refetchInterval: 5000 }) + .thenReturn(RUN_STATUS_AWAITING_RECOVERY) + render(`/runs/${RUN_ID}/run`) + expect(vi.mocked(RunPausedSplash)).toHaveBeenCalled() + }) + // ToDo (kj:04/04/2023) need to figure out the way to simulate swipe it.todo('should render RunningProtocolCommandList when swiping left') // const [{ getByText }] = render(`/runs/${RUN_ID}/run`) diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 2fc56806679f..3ebe3b1c0aba 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -25,8 +25,10 @@ import { import { RUN_STATUS_STOP_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' +import { useFeatureFlag } from '../../redux/config' import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { @@ -51,6 +53,7 @@ import { } from '../../organisms/Devices/hooks' import { CancelingRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal' import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal' +import { RunPausedSplash } from '../../organisms/OnDeviceDisplay/RunningProtocol/RunPausedSplash' import { getLocalRobot } from '../../redux/discovery' import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' @@ -102,6 +105,7 @@ export function RunningProtocol(): JSX.Element { const runStatus = useRunStatus(runId, { refetchInterval: RUN_STATUS_REFETCH_INTERVAL, }) + const [enableSplash, setEnableSplash] = React.useState(true) const { startedAt, stoppedAt, completedAt } = useRunTimestamps(runId) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const protocolId = runRecord?.data.protocolId ?? null @@ -117,6 +121,10 @@ export function RunningProtocol(): JSX.Element { const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) const robotType = useRobotType(robotName) + const errorType = runRecord?.data.errors[0]?.errorType + + const enableRunNotes = useFeatureFlag('enableRunNotes') + React.useEffect(() => { if ( currentOption === 'CurrentRunningProtocolCommand' && @@ -160,114 +168,137 @@ export function RunningProtocol(): JSX.Element { return ( <> - {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( - - ) : null} - {runStatus === RUN_STATUS_STOP_REQUESTED ? : null} - - {robotSideAnalysis != null ? ( - - ) : null} - {showConfirmCancelRunModal ? ( - - ) : null} - {interventionModalCommandKey != null && - runRecord?.data != null && - lastRunCommand != null && - isInterventionCommand(lastRunCommand) ? ( - - ) : null} - - {robotSideAnalysis != null ? ( - currentOption === 'CurrentRunningProtocolCommand' ? ( - - (lastAnimatedCommand.current = newCommandKey) + {enableSplash && + runStatus === RUN_STATUS_AWAITING_RECOVERY && + enableRunNotes ? ( + setEnableSplash(false)} + errorType={errorType} + protocolName={protocolName} + /> + ) : ( + <> + {runStatus === RUN_STATUS_BLOCKED_BY_OPEN_DOOR ? ( + + ) : null} + {runStatus === RUN_STATUS_STOP_REQUESTED ? ( + + ) : null} + + {robotSideAnalysis != null ? ( + - ) : ( - <> - + ) : null} + {interventionModalCommandKey != null && + runRecord?.data != null && + lastRunCommand != null && + isInterventionCommand(lastRunCommand) ? ( + + ) : null} + + {robotSideAnalysis != null ? ( + currentOption === 'CurrentRunningProtocolCommand' ? ( + + (lastAnimatedCommand.current = newCommandKey) + } + /> + ) : ( + <> + + + + ) + ) : ( + + )} + + - - - ) - ) : ( - - )} - - - + + - - + + )} ) } diff --git a/app/src/pages/Welcome/index.tsx b/app/src/pages/Welcome/index.tsx index 47bb9cbcb503..f5c1ac686bd2 100644 --- a/app/src/pages/Welcome/index.tsx +++ b/app/src/pages/Welcome/index.tsx @@ -17,7 +17,7 @@ import screenImage from '../../assets/images/on-device-display/welcome_backgroun const IMAGE_ALT = 'Welcome screen background image' export function Welcome(): JSX.Element { - const { t } = useTranslation(['device_settings', 'shared']) + const { t } = useTranslation(['device_settings', 'shared', 'branded']) const history = useHistory() return ( @@ -30,7 +30,7 @@ export function Welcome(): JSX.Element { {IMAGE_ALT} - {t('welcome_title')} + {t('branded:welcome_title')} diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 1dc64fea2f47..5a72622f98e7 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -2,7 +2,6 @@ import type { DevInternalFlag } from './types' export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ 'protocolStats', - 'enableRunTimeParameters', 'enableRunNotes', 'enableQuickTransfer', ] diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index e69186f5f070..5728a2e4eb1a 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -9,7 +9,6 @@ export type DiscoveryCandidates = string[] export type DevInternalFlag = | 'protocolStats' - | 'enableRunTimeParameters' | 'enableRunNotes' | 'enableQuickTransfer' diff --git a/app/src/redux/protocol-storage/__fixtures__/index.ts b/app/src/redux/protocol-storage/__fixtures__/index.ts index 12e350efb387..56f7f4d021a5 100644 --- a/app/src/redux/protocol-storage/__fixtures__/index.ts +++ b/app/src/redux/protocol-storage/__fixtures__/index.ts @@ -1,5 +1,5 @@ import { simpleAnalysisFileFixture } from '@opentrons/api-client' -import { StoredProtocolData, StoredProtocolDir } from '../types' +import type { StoredProtocolData, StoredProtocolDir } from '../types' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' diff --git a/app/src/redux/robot-settings/types.ts b/app/src/redux/robot-settings/types.ts index 5571be6a4417..3f998311c46d 100644 --- a/app/src/redux/robot-settings/types.ts +++ b/app/src/redux/robot-settings/types.ts @@ -1,20 +1,10 @@ +import type { + RobotSettings, + RobotSettingsField, + RobotSettingsResponse, +} from '@opentrons/api-client' import type { RobotApiRequestMeta } from '../robot-api/types' -export interface RobotSettingsField { - id: string - title: string - description: string - value: boolean | null - restart_required?: boolean -} - -export type RobotSettings = RobotSettingsField[] - -export interface RobotSettingsResponse { - settings: RobotSettings - links?: { restart?: string } -} - export interface PerRobotRobotSettingsState { settings: RobotSettings restartPath: string | null @@ -94,3 +84,6 @@ export type RobotSettingsAction = | UpdateSettingAction | UpdateSettingSuccessAction | UpdateSettingFailureAction + +// TODO(bh, 2024-03-26): update type imports elsewhere to @opentrons/api-client +export type { RobotSettings, RobotSettingsField, RobotSettingsResponse } diff --git a/app/src/redux/shell/index.ts b/app/src/redux/shell/index.ts index a709a770d7ff..5a918f75eb35 100644 --- a/app/src/redux/shell/index.ts +++ b/app/src/redux/shell/index.ts @@ -5,4 +5,4 @@ export * from './update' export * from './is-ready/actions' export * from './is-ready/selectors' -export const CURRENT_VERSION: string = (global as any)._PKG_VERSION_ +export const CURRENT_VERSION: string = _PKG_VERSION_ diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 1a4cb343d642..d83cee94b154 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -20,7 +20,7 @@ export type IpcListener = ( ) => void export interface NotifyRefetchData { - refetchUsingHTTP: boolean + refetch: boolean } export interface NotifyUnsubscribeData { diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index 1e2ba78c744c..fdb531ab1cdb 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -53,7 +53,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -68,7 +68,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, forceHttpPolling: true }, } as any) ) @@ -81,7 +81,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, enabled: false }, } as any) ) @@ -94,7 +94,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: { ...MOCK_OPTIONS, staleTime: Infinity }, } as any) ) @@ -111,7 +111,7 @@ describe('useNotifyService', () => { renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -128,7 +128,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -142,12 +142,12 @@ describe('useNotifyService', () => { callback, }): any { // eslint-disable-next-line n/no-callback-literal - callback({ refetchUsingHTTP: true }) + callback({ refetch: true }) }) const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -165,7 +165,7 @@ describe('useNotifyService', () => { const { rerender } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, } as any) ) @@ -177,11 +177,27 @@ describe('useNotifyService', () => { const { unmount } = renderHook(() => useNotifyService({ topic: MOCK_TOPIC, - setRefetchUsingHTTP: mockHTTPRefetch, + setRefetch: mockHTTPRefetch, options: MOCK_OPTIONS, }) ) unmount() expect(appShellListener).toHaveBeenCalled() }) + + it('should still clean up the listener if the hostname changes to null after subscribing', () => { + const { unmount, rerender } = renderHook(() => + useNotifyService({ + hostOverride: MOCK_HOST_CONFIG, + topic: MOCK_TOPIC, + setRefetch: mockHTTPRefetch, + options: MOCK_OPTIONS, + }) + ) + rerender({ hostOverride: null }) + unmount() + expect(appShellListener).toHaveBeenCalledWith( + expect.objectContaining({ hostname: MOCK_HOST_CONFIG.hostname }) + ) + }) }) diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 95b92e9f7dce..beae36d9821e 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -7,13 +7,9 @@ import { getCutoutIdForAddressableArea, getDeckDefFromRobotType, getLabwareDisplayName, - SINGLE_LEFT_SLOT_FIXTURE, SINGLE_SLOT_FIXTURES, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' - import type { CompletedProtocolAnalysis, CutoutConfigProtocolSpec, @@ -47,17 +43,6 @@ export function useDeckConfigurationCompatibility( ? getInitialAndMovedLabwareInSlots(protocolAnalysis) : [] - const protocolModulesInfo = - protocolAnalysis != null - ? getProtocolModulesInfo(protocolAnalysis, deckDef) - : [] - - const hasThermocycler = - protocolModulesInfo.find( - protocolMod => - protocolMod.moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE - ) != null - return deckConfig.reduce( (acc, { cutoutId, cutoutFixtureId }) => { const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId( @@ -69,7 +54,6 @@ export function useDeckConfigurationCompatibility( getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) === cutoutId ) - const compatibleCutoutFixtureIds = fixturesThatMountToCutoutId .filter(cf => requiredAddressableAreasForCutoutId.every(aa => @@ -106,11 +90,7 @@ export function useDeckConfigurationCompatibility( cutoutId, cutoutFixtureId, requiredAddressableAreas: requiredAddressableAreasForCutoutId, - // Thermocycler requires an "empty" (single slot) fixture in A1 that is not referenced directly in protocol - compatibleCutoutFixtureIds: - hasThermocycler && cutoutId === 'cutoutA1' - ? [SINGLE_LEFT_SLOT_FIXTURE] - : compatibleCutoutFixtureIds, + compatibleCutoutFixtureIds, missingLabwareDisplayName, }, ] diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 9efaeea3a577..5306b967d4b1 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -25,8 +25,9 @@ export function getIsFixtureMismatch( deckConfigProtocolSpec: CutoutConfigAndCompatibility[] ): boolean { const isFixtureMismatch = !deckConfigProtocolSpec.every( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => - isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds) + ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => { + return isMatchedFixture(cutoutFixtureId, compatibleCutoutFixtureIds) + } ) return isFixtureMismatch } diff --git a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts index 2692e032d6af..28859afe3930 100644 --- a/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts +++ b/app/src/resources/maintenance_runs/useNotifyCurrentMaintenanceRun.ts @@ -14,24 +14,18 @@ import type { export function useNotifyCurrentMaintenanceRun( options: QueryOptionsWithPolling = {} ): UseQueryResult | UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/maintenance_runs/current_run', - setRefetchUsingHTTP, + setRefetch, options, }) const httpQueryResult = useCurrentMaintenanceRun({ ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpQueryResult diff --git a/app/src/resources/runs/useNotifyAllRunsQuery.ts b/app/src/resources/runs/useNotifyAllRunsQuery.ts index 690d7a4ac11e..1ae93ffc7130 100644 --- a/app/src/resources/runs/useNotifyAllRunsQuery.ts +++ b/app/src/resources/runs/useNotifyAllRunsQuery.ts @@ -18,14 +18,11 @@ export function useNotifyAllRunsQuery( options: QueryOptionsWithPolling = {}, hostOverride?: HostConfig | null ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs', - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }) @@ -34,11 +31,8 @@ export function useNotifyAllRunsQuery( params, { ...(options as UseAllRunsQueryOptions), - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }, hostOverride ) diff --git a/app/src/resources/runs/useNotifyLastRunCommandKey.ts b/app/src/resources/runs/useNotifyLastRunCommandKey.ts index 8600c4d66b60..9c908a70749b 100644 --- a/app/src/resources/runs/useNotifyLastRunCommandKey.ts +++ b/app/src/resources/runs/useNotifyLastRunCommandKey.ts @@ -13,24 +13,18 @@ export function useNotifyLastRunCommandKey( runId: string, options: QueryOptionsWithPolling = {} ): string | null { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) useNotifyService({ topic: 'robot-server/runs/current_command', - setRefetchUsingHTTP, + setRefetch, options, }) const httpResponse = useLastRunCommandKey(runId, { ...options, - enabled: options?.enabled !== false && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: options?.enabled !== false && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/runs/useNotifyRunQuery.ts b/app/src/resources/runs/useNotifyRunQuery.ts index dde7bc844487..2ca72687341f 100644 --- a/app/src/resources/runs/useNotifyRunQuery.ts +++ b/app/src/resources/runs/useNotifyRunQuery.ts @@ -16,26 +16,20 @@ export function useNotifyRunQuery( runId: string | null, options: QueryOptionsWithPolling = {} ): UseQueryResult { - const [ - refetchUsingHTTP, - setRefetchUsingHTTP, - ] = React.useState(null) + const [refetch, setRefetch] = React.useState(null) const isEnabled = options.enabled !== false && runId != null useNotifyService({ topic: `robot-server/runs/${runId}` as NotifyTopic, - setRefetchUsingHTTP, + setRefetch, options: { ...options, enabled: options.enabled != null && runId != null }, }) const httpResponse = useRunQuery(runId, { ...options, - enabled: isEnabled && refetchUsingHTTP != null, - onSettled: - refetchUsingHTTP === 'once' - ? () => setRefetchUsingHTTP(null) - : () => null, + enabled: isEnabled && refetch != null, + onSettled: refetch === 'once' ? () => setRefetch(null) : () => null, }) return httpResponse diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index f6cfaefa2b88..19831dc9c629 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -25,14 +25,14 @@ export interface QueryOptionsWithPolling interface UseNotifyServiceProps { topic: NotifyTopic - setRefetchUsingHTTP: (refetch: HTTPRefetchFrequency) => void + setRefetch: (refetch: HTTPRefetchFrequency) => void options: QueryOptionsWithPolling hostOverride?: HostConfig | null } export function useNotifyService({ topic, - setRefetchUsingHTTP, + setRefetch, options, hostOverride, }: UseNotifyServiceProps): void { @@ -42,7 +42,7 @@ export function useNotifyService({ const hostname = host?.hostname ?? null const doTrackEvent = useTrackEvent() const isFlex = useIsFlex(host?.robotName ?? '') - const hasUsedNotifyService = React.useRef(false) + const seenHostname = React.useRef(null) const { enabled, staleTime, forceHttpPolling } = options const shouldUseNotifications = @@ -54,33 +54,33 @@ export function useNotifyService({ React.useEffect(() => { if (shouldUseNotifications) { // Always fetch on initial mount. - setRefetchUsingHTTP('once') + setRefetch('once') appShellListener({ hostname, topic, callback: onDataEvent, }) dispatch(notifySubscribeAction(hostname, topic)) - hasUsedNotifyService.current = true + seenHostname.current = hostname } else { - setRefetchUsingHTTP('always') + setRefetch('always') } return () => { - if (hasUsedNotifyService.current) { + if (seenHostname.current != null) { appShellListener({ - hostname: hostname as string, + hostname: seenHostname.current, topic, callback: onDataEvent, isDismounting: true, }) } } - }, [topic, host, shouldUseNotifications]) + }, [topic, hostname, shouldUseNotifications]) function onDataEvent(data: NotifyResponseData): void { if (data === 'ECONNFAILED' || data === 'ECONNREFUSED') { - setRefetchUsingHTTP('always') + setRefetch('always') // TODO(jh 2023-02-23): remove the robot type check once OT-2s support MQTT. if (data === 'ECONNREFUSED' && isFlex) { doTrackEvent({ @@ -88,8 +88,8 @@ export function useNotifyService({ properties: {}, }) } - } else if ('refetchUsingHTTP' in data || 'unsubscribe' in data) { - setRefetchUsingHTTP('once') + } else if ('refetch' in data || 'unsubscribe' in data) { + setRefetch('once') } } } diff --git a/app/typings/global.d.ts b/app/typings/global.d.ts index 772bcf9ffd00..de7366272407 100644 --- a/app/typings/global.d.ts +++ b/app/typings/global.d.ts @@ -1,6 +1,4 @@ declare const global: typeof globalThis & { - _PKG_VERSION_: string - _OPENTRONS_PROJECT_: string APP_SHELL_REMOTE: { // sa 02-02-2024 any typing this because importing the IpcRenderer type // from electron makes this ambient type declaration a module instead of @@ -9,3 +7,6 @@ declare const global: typeof globalThis & { [key: string]: any } } + +declare const _PKG_VERSION_: string +declare const _OPENTRONS_PROJECT_: string diff --git a/app/typings/remark.d.ts b/app/typings/remark.d.ts deleted file mode 100644 index 2eb55d6f77ec..000000000000 --- a/app/typings/remark.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module 'remark' { - const remark: any - // eslint-disable-next-line import/no-default-export - export default remark -} - -declare module 'remark-react' { - const reactRenderer: any - // eslint-disable-next-line import/no-default-export - export default reactRenderer -} diff --git a/app/vite.config.ts b/app/vite.config.ts index 9710acdd2404..f88d492056af 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -6,57 +6,66 @@ import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import { versionForProject } from '../scripts/git-version.mjs' +import type { UserConfig } from 'vite' -export default defineConfig({ - // this makes imports relative rather than absolute - base: '', - build: { - // Relative to the root - outDir: 'dist', - }, - plugins: [ - react({ - include: '**/*.tsx', - babel: { - // Use babel.config.js files - configFile: true, +export default defineConfig( + async(): Promise => { + const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' + const version = await versionForProject(project) + return { + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', }, - }), - ], - optimizeDeps: { - esbuildOptions: { - target: 'es2020', - }, - }, - css: { - postcss: { plugins: [ - postCssImport({ root: 'src/' }), - postCssApply(), - postColorModFunction(), - postCssPresetEnv({ stage: 0 }), - lostCss(), + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), ], - }, - }, - define: { - 'process.env': process.env, - global: 'globalThis', - }, - resolve: { - alias: { - '@opentrons/components/styles': path.resolve( - '../components/src/index.module.css' - ), - '@opentrons/components': path.resolve('../components/src/index.ts'), - '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), - '@opentrons/step-generation': path.resolve( - '../step-generation/src/index.ts' - ), - '@opentrons/api-client': path.resolve('../api-client/src/index.ts'), - '@opentrons/react-api-client': path.resolve( - '../react-api-client/src/index.ts' - ), - }, - }, -}) + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [ + postCssImport({ root: 'src/' }), + postCssApply(), + postColorModFunction(), + postCssPresetEnv({ stage: 0 }), + lostCss(), + ], + }, + }, + define: { + 'process.env': process.env, + global: 'globalThis', + _PKG_VERSION_: JSON.stringify(version), + _OPENTRONS_PROJECT_: JSON.stringify(project), + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), + '@opentrons/step-generation': path.resolve( + '../step-generation/src/index.ts' + ), + '@opentrons/api-client': path.resolve('../api-client/src/index.ts'), + '@opentrons/react-api-client': path.resolve( + '../react-api-client/src/index.ts' + ), + }, + }, + } + }) diff --git a/components/src/atoms/Chip/Chip.stories.tsx b/components/src/atoms/Chip/Chip.stories.tsx index 2868d7246f72..027ea4cbdbec 100644 --- a/components/src/atoms/Chip/Chip.stories.tsx +++ b/components/src/atoms/Chip/Chip.stories.tsx @@ -14,27 +14,23 @@ const meta: Meta = { control: { type: 'select', }, - defaultValue: 'basic', }, hasIcon: { control: { type: 'boolean', }, - defaultValue: true, }, chipSize: { options: ['medium', 'small'], control: { type: 'select', }, - defaultValue: 'medium', }, iconName: { options: ['connection-status', 'ot-check', 'ot-alert'], control: { type: 'select', }, - defaultValue: 'ot-alert', }, }, component: Chip, @@ -57,7 +53,7 @@ type Story = StoryObj export const ChipComponent: Story = { args: { - type: 'basic', + type: 'success', text: 'Chip component', hasIcon: true, chipSize: 'medium', diff --git a/components/src/atoms/StyledText/StyledText.stories.tsx b/components/src/atoms/StyledText/StyledText.stories.tsx index 12f8ab8c16a2..388f7e79bdf2 100644 --- a/components/src/atoms/StyledText/StyledText.stories.tsx +++ b/components/src/atoms/StyledText/StyledText.stories.tsx @@ -1,87 +1,107 @@ +/* eslint-disable storybook/prefer-pascal-case */ import * as React from 'react' -import { StyledText } from './' -import { TYPOGRAPHY } from '../../ui-style-constants' -import type { Story, Meta } from '@storybook/react' +import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { Flex } from '../../primitives' +import { StyledText } from './index' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/StyledText', component: StyledText, -} as Meta + decorators: [ + Story => ( + + + + ), + ], +} + +export default meta -const Template: Story> = args => ( - -) +type Story = StoryObj const dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus sapien nunc dolor, aliquet nibh placerat et nisl, arcu. Pellentesque blandit sollicitudin vitae morbi morbi vulputate cursus tellus. Amet proin donec proin id aliquet in nullam.' -export const h1 = Template.bind({}) -h1.args = { - as: 'h1', - children: dummyText, +export const h1: Story = { + args: { + as: 'h1', + children: dummyText, + }, } -export const h2 = Template.bind({}) -h2.args = { - as: 'h2', - children: dummyText, +export const h2: Story = { + args: { + as: 'h2', + children: dummyText, + }, } -export const h3 = Template.bind({}) -h3.args = { - as: 'h3', - children: dummyText, +export const h3: Story = { + args: { + as: 'h3', + children: dummyText, + }, } -export const h6 = Template.bind({}) -h6.args = { - as: 'h6', - children: dummyText, +export const h6: Story = { + args: { + as: 'h6', + children: dummyText, + }, } -export const p = Template.bind({}) -p.args = { - as: 'p', - children: dummyText, +export const p: Story = { + args: { + as: 'p', + children: dummyText, + }, } -export const label = Template.bind({}) -label.args = { - as: 'label', - children: dummyText, +export const label: Story = { + args: { + as: 'label', + children: dummyText, + }, } -export const h2SemiBold = Template.bind({}) -h2SemiBold.args = { - as: 'h2', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h2SemiBold: Story = { + args: { + as: 'h2', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const h3SemiBold = Template.bind({}) -h3SemiBold.args = { - as: 'h3', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h3SemiBold: Story = { + args: { + as: 'h3', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const h6SemiBold = Template.bind({}) -h6SemiBold.args = { - as: 'h6', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const h6SemiBold: Story = { + args: { + as: 'h6', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const pSemiBold = Template.bind({}) -pSemiBold.args = { - as: 'p', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const pSemiBold: Story = { + args: { + as: 'p', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } -export const labelSemiBold = Template.bind({}) -labelSemiBold.args = { - as: 'label', - fontWeight: TYPOGRAPHY.fontWeightSemiBold, - children: dummyText, +export const labelSemiBold: Story = { + args: { + as: 'label', + fontWeight: TYPOGRAPHY.fontWeightSemiBold, + children: dummyText, + }, } diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index e664cb10277f..d896c0c9370b 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -15,6 +15,7 @@ import { WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, HEATERSHAKER_MODULE_V1, + MODULE_FIXTURES_BY_MODEL, } from '@opentrons/shared-data' import { RobotCoordinateSpace } from '../RobotCoordinateSpace' @@ -32,6 +33,7 @@ import { WasteChuteFixture } from './WasteChuteFixture' import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' import type { + CutoutFixtureId, DeckConfiguration, LabwareDefinition2, LabwareLocation, @@ -101,7 +103,14 @@ export function BaseDeck(props: BaseDeckProps): JSX.Element { const singleSlotFixtures = deckConfig.filter( fixture => fixture.cutoutFixtureId != null && - SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + (SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) || + // If module fixture is loaded, still visualize singleSlotFixture underneath for consistency + Object.entries(MODULE_FIXTURES_BY_MODEL) + .reduce( + (acc, [_model, fixtures]) => [...acc, ...fixtures], + [] + ) + .includes(fixture.cutoutFixtureId)) ) const stagingAreaFixtures = deckConfig.filter( fixture => diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx index dc900541fd6a..f29b2cffc022 100644 --- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx +++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx @@ -71,6 +71,12 @@ const deckConfig: CutoutConfig[] = [ }, ] +const staticFixtures = [ + { location: 'cutoutB2', label: 'Tip rack' }, + { location: 'cutoutC2', label: 'Labware' }, + { location: 'cutoutD2', label: 'Labware' }, +] + export const Default = Template.bind({}) Default.args = { deckConfig, @@ -85,3 +91,12 @@ ReadOnly.args = { handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`), readOnly: true, } + +export const ReadOnlyWithStaticFixtures = Template.bind({}) +ReadOnlyWithStaticFixtures.args = { + deckConfig, + handleClickAdd: () => {}, + handleClickRemove: () => {}, + readOnly: true, + additionalStaticFixtures: staticFixtures, +} diff --git a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx index fba7dfc57cf9..d790f2e922de 100644 --- a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx @@ -8,10 +8,13 @@ import { RESPONSIVENESS } from '../../ui-style-constants' import { BORDERS, COLORS } from '../../helix-design-system' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' import { + COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, COLUMN_1_X_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, COLUMN_3_X_ADJUSTMENT, FIXTURE_HEIGHT, - SINGLE_SLOT_FIXTURE_WIDTH, Y_ADJUSTMENT, } from './constants' @@ -39,22 +42,40 @@ export function EmptyConfigFixture( */ const [xSlotPosition = 0, ySlotPosition = 0] = standardSlotCutout?.position ?? [] - - const isColumnOne = - fixtureLocation === 'cutoutA1' || - fixtureLocation === 'cutoutB1' || - fixtureLocation === 'cutoutC1' || - fixtureLocation === 'cutoutD1' - const xAdjustment = isColumnOne - ? COLUMN_1_X_ADJUSTMENT - : COLUMN_3_X_ADJUSTMENT - const x = xSlotPosition + xAdjustment + let x = xSlotPosition + let width = 0 + switch (fixtureLocation) { + case 'cutoutA1': + case 'cutoutB1': + case 'cutoutC1': + case 'cutoutD1': { + x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + width = COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA2': + case 'cutoutB2': + case 'cutoutC2': + case 'cutoutD2': { + x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + width = COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA3': + case 'cutoutB3': + case 'cutoutC3': + case 'cutoutD3': { + x = xSlotPosition + COLUMN_3_X_ADJUSTMENT + width = COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH + break + } + } const y = ySlotPosition + Y_ADJUSTMENT return ( void +} + +const HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME = 'Heater-Shaker' + +export function HeaterShakerFixture( + props: HeaterShakerFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + + const isColumnOne = + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' + const xAdjustment = isColumnOne + ? COLUMN_1_X_ADJUSTMENT + : COLUMN_3_X_ADJUSTMENT + const x = xSlotPosition + xAdjustment + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx b/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx new file mode 100644 index 000000000000..a9f1485c2bdd --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/MagneticBlockFixture.tsx @@ -0,0 +1,130 @@ +import * as React from 'react' + +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_1_X_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, + COLUMN_3_X_ADJUSTMENT, + FIXTURE_HEIGHT, + Y_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + STAGING_AREA_FIXTURE_WIDTH, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +interface MagneticBlockFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void + hasStagingArea?: boolean +} + +const MAGNETIC_BLOCK_FIXTURE_DISPLAY_NAME = 'Mag Block' +const STAGING_AREA_WITH_MAGNETIC_BLOCK_DISPLAY_NAME = 'Mag + staging' + +export function MagneticBlockFixture( + props: MagneticBlockFixtureProps +): JSX.Element { + const { + deckDefinition, + fixtureLocation, + handleClickRemove, + cutoutFixtureId, + hasStagingArea, + } = props + + const standardSlotCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = + standardSlotCutout?.position ?? [] + let x = xSlotPosition + let width = 0 + let displayName = hasStagingArea + ? STAGING_AREA_WITH_MAGNETIC_BLOCK_DISPLAY_NAME + : MAGNETIC_BLOCK_FIXTURE_DISPLAY_NAME + switch (fixtureLocation) { + case 'cutoutA1': + case 'cutoutB1': + case 'cutoutC1': + case 'cutoutD1': { + x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + width = COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH + break + } + case 'cutoutA2': + case 'cutoutB2': + case 'cutoutC2': + case 'cutoutD2': { + x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + width = COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH + displayName = 'Mag' + break + } + case 'cutoutA3': + case 'cutoutB3': + case 'cutoutC3': + case 'cutoutD3': { + x = xSlotPosition + COLUMN_3_X_ADJUSTMENT + width = hasStagingArea + ? STAGING_AREA_FIXTURE_WIDTH + : COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH + break + } + } + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + {displayName} + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx index d1cd3af27d02..2ab3de6c3be8 100644 --- a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx @@ -15,18 +15,31 @@ import { Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface StagingAreaConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void } export function StagingAreaConfigFixture( props: StagingAreaConfigFixtureProps ): JSX.Element { - const { deckDefinition, handleClickRemove, fixtureLocation } = props + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props const stagingAreaCutout = deckDefinition.locations.cutouts.find( cutout => cutout.id === fixtureLocation @@ -60,7 +73,7 @@ export function StagingAreaConfigFixture( cursor={handleClickRemove != null ? 'pointer' : 'default'} onClick={ handleClickRemove != null - ? () => handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx new file mode 100644 index 000000000000..a0fcd1d97d13 --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' + +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + CONFIG_STYLE_READ_ONLY, + FIXTURE_HEIGHT, + COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH, + Y_ADJUSTMENT, + COLUMN_2_X_ADJUSTMENT, +} from './constants' + +import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' + +interface StaticFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + label: string +} + +/** + * this component allows us to add static labeled fixtures to the center column of a deck + * config map + */ + +export function StaticFixture(props: StaticFixtureProps): JSX.Element { + const { deckDefinition, fixtureLocation, label } = props + + const staticCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + */ + const [xSlotPosition = 0, ySlotPosition = 0] = staticCutout?.position ?? [] + const y = ySlotPosition + Y_ADJUSTMENT + const x = xSlotPosition + COLUMN_2_X_ADJUSTMENT + + return ( + + {}}> + {label} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/TemperatureModuleFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TemperatureModuleFixture.tsx new file mode 100644 index 000000000000..54d2a5aec6a2 --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/TemperatureModuleFixture.tsx @@ -0,0 +1,101 @@ +import * as React from 'react' +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_1_X_ADJUSTMENT, + COLUMN_3_X_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + FIXTURE_HEIGHT, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, + Y_ADJUSTMENT, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +// TODO(BC, 2024-03-21): This component is almost identical to HeaterShakerFixture, consider consolidating? + +const TEMPERATURE_MODULE_FIXTURE_DISPLAY_NAME = 'Temperature' + +interface TemperatureModuleFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void +} + +export function TemperatureModuleFixture( + props: TemperatureModuleFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + + const isColumnOne = + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' + const xAdjustment = isColumnOne + ? COLUMN_1_X_ADJUSTMENT + : COLUMN_3_X_ADJUSTMENT + const x = xSlotPosition + xAdjustment + + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {TEMPERATURE_MODULE_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx b/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx new file mode 100644 index 000000000000..83369a736ad4 --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/ThermocyclerFixture.tsx @@ -0,0 +1,88 @@ +import * as React from 'react' +import { Icon } from '../../icons' +import { Btn, Text } from '../../primitives' +import { TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS } from '../../helix-design-system' +import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + COLUMN_1_X_ADJUSTMENT, + CONFIG_STYLE_EDITABLE, + CONFIG_STYLE_READ_ONLY, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, + Y_ADJUSTMENT, + THERMOCYCLER_FIXTURE_HEIGHT, +} from './constants' + +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' + +interface ThermocyclerFixtureProps { + deckDefinition: DeckDefinition + fixtureLocation: CutoutId + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void +} + +const THERMOCYCLER_FIXTURE_DISPLAY_NAME = 'Thermocycler' + +export function ThermocyclerFixture( + props: ThermocyclerFixtureProps +): JSX.Element { + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props + + const cutoutDef = deckDefinition.locations.cutouts.find( + cutout => cutout.id === fixtureLocation + ) + + /** + * deck definition cutout position is the position of the single slot located within that cutout + * so, to get the position of the cutout itself we must add an adjustment to the slot position + * the adjustment for x is different for right side/left side + */ + const [xSlotPosition = 0, ySlotPosition = 0] = cutoutDef?.position ?? [] + const x = xSlotPosition + COLUMN_1_X_ADJUSTMENT + const y = ySlotPosition + Y_ADJUSTMENT + + return ( + + handleClickRemove(fixtureLocation, cutoutFixtureId) + : () => {} + } + > + + {THERMOCYCLER_FIXTURE_DISPLAY_NAME} + + {handleClickRemove != null ? ( + + ) : null} + + + ) +} diff --git a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx index 9a9a100d92f4..5dd31a577503 100644 --- a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx @@ -11,23 +11,36 @@ import { CONFIG_STYLE_EDITABLE, CONFIG_STYLE_READ_ONLY, FIXTURE_HEIGHT, - SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, TRASH_BIN_DISPLAY_NAME, Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface TrashBinConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void } export function TrashBinConfigFixture( props: TrashBinConfigFixtureProps ): JSX.Element { - const { deckDefinition, handleClickRemove, fixtureLocation } = props + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + cutoutFixtureId, + } = props const trashBinCutout = deckDefinition.locations.cutouts.find( cutout => cutout.id === fixtureLocation @@ -54,7 +67,7 @@ export function TrashBinConfigFixture( return ( handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx index 1932fe3674bd..d1e3c5959f25 100644 --- a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx @@ -11,17 +11,25 @@ import { CONFIG_STYLE_READ_ONLY, FIXTURE_HEIGHT, STAGING_AREA_FIXTURE_WIDTH, - SINGLE_SLOT_FIXTURE_WIDTH, + COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, WASTE_CHUTE_DISPLAY_NAME, Y_ADJUSTMENT, } from './constants' -import type { CutoutId, DeckDefinition } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckDefinition, +} from '@opentrons/shared-data' interface WasteChuteConfigFixtureProps { deckDefinition: DeckDefinition fixtureLocation: CutoutId - handleClickRemove?: (fixtureLocation: CutoutId) => void + cutoutFixtureId: CutoutFixtureId + handleClickRemove?: ( + fixtureLocation: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void hasStagingAreas?: boolean } @@ -32,6 +40,7 @@ export function WasteChuteConfigFixture( deckDefinition, handleClickRemove, fixtureLocation, + cutoutFixtureId, hasStagingAreas = false, } = props @@ -52,7 +61,9 @@ export function WasteChuteConfigFixture( return ( handleClickRemove(fixtureLocation) + ? () => handleClickRemove(fixtureLocation, cutoutFixtureId) : () => {} } > diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts index 53faef10b7ea..4b47b9c5917c 100644 --- a/components/src/hardware-sim/DeckConfigurator/constants.ts +++ b/components/src/hardware-sim/DeckConfigurator/constants.ts @@ -9,10 +9,14 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' * Position is relative to deck definition slot positions and a custom stroke applied to the single slot fixture SVG */ export const FIXTURE_HEIGHT = 102.0 -export const SINGLE_SLOT_FIXTURE_WIDTH = 243.5 +export const THERMOCYCLER_FIXTURE_HEIGHT = 290.0 +export const COLUMN_1_SINGLE_SLOT_FIXTURE_WIDTH = 243.5 +export const COLUMN_2_SINGLE_SLOT_FIXTURE_WIDTH = 159.0 +export const COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH = 243.5 export const STAGING_AREA_FIXTURE_WIDTH = 314.5 export const COLUMN_1_X_ADJUSTMENT = -100 +export const COLUMN_2_X_ADJUSTMENT = -15.5 export const COLUMN_3_X_ADJUSTMENT = -15.5 export const Y_ADJUSTMENT = -8 diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 9378471d8e07..8de6ba4da701 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -8,6 +8,11 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_ONLY_FIXTURES, WASTE_CHUTE_STAGING_AREA_FIXTURES, + THERMOCYCLER_V2_FRONT_FIXTURE, + HEATERSHAKER_MODULE_V1_FIXTURE, + TEMPERATURE_MODULE_V2_FIXTURE, + MAGNETIC_BLOCK_V1_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, } from '@opentrons/shared-data' import { COLORS } from '../../helix-design-system' @@ -18,18 +23,32 @@ import { EmptyConfigFixture } from './EmptyConfigFixture' import { StagingAreaConfigFixture } from './StagingAreaConfigFixture' import { TrashBinConfigFixture } from './TrashBinConfigFixture' import { WasteChuteConfigFixture } from './WasteChuteConfigFixture' +import { StaticFixture } from './StaticFixture' -import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' +import type { + CutoutFixtureId, + CutoutId, + DeckConfiguration, +} from '@opentrons/shared-data' +import { TemperatureModuleFixture } from './TemperatureModuleFixture' +import { HeaterShakerFixture } from './HeaterShakerFixture' +import { MagneticBlockFixture } from './MagneticBlockFixture' +import { ThermocyclerFixture } from './ThermocyclerFixture' interface DeckConfiguratorProps { deckConfig: DeckConfiguration handleClickAdd: (cutoutId: CutoutId) => void - handleClickRemove: (cutoutId: CutoutId) => void + handleClickRemove: ( + cutoutId: CutoutId, + cutoutFixtureId: CutoutFixtureId + ) => void lightFill?: string darkFill?: string - readOnly?: boolean + editableCutoutIds?: CutoutId[] showExpansion?: boolean children?: React.ReactNode + additionalStaticFixtures?: Array<{ location: CutoutId; label: string }> + height?: string } export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -37,56 +56,57 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { deckConfig, handleClickAdd, handleClickRemove, + additionalStaticFixtures, + children, lightFill = COLORS.grey35, darkFill = COLORS.black90, - readOnly = false, + editableCutoutIds = deckConfig.map(({ cutoutId }) => cutoutId), showExpansion = true, - children, + height = '455px', } = props const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) - // restrict configuration to certain locations - const configurableFixtureLocations: CutoutId[] = [ - 'cutoutA1', - 'cutoutB1', - 'cutoutC1', - 'cutoutD1', - 'cutoutA3', - 'cutoutB3', - 'cutoutC3', - 'cutoutD3', - ] - const configurableDeckConfig = deckConfig.filter(({ cutoutId }) => - configurableFixtureLocations.includes(cutoutId) - ) - - const stagingAreaFixtures = configurableDeckConfig.filter( + const stagingAreaFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) - const wasteChuteFixtures = configurableDeckConfig.filter( + const wasteChuteFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId != null && WASTE_CHUTE_ONLY_FIXTURES.includes(cutoutFixtureId) ) - const wasteChuteStagingAreaFixtures = configurableDeckConfig.filter( + const wasteChuteStagingAreaFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId != null && WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(cutoutFixtureId) ) - const emptyFixtures = readOnly - ? [] - : configurableDeckConfig.filter( - ({ cutoutFixtureId }) => - cutoutFixtureId != null && - SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) - ) - const trashBinFixtures = configurableDeckConfig.filter( + const emptyCutouts = deckConfig.filter( + ({ cutoutFixtureId, cutoutId }) => + editableCutoutIds.includes(cutoutId) && + cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) + const trashBinFixtures = deckConfig.filter( ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) + const thermocyclerFixtures = deckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE + ) + const heaterShakerFixtures = deckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === HEATERSHAKER_MODULE_V1_FIXTURE + ) + const temperatureModuleFixtures = deckConfig.filter( + ({ cutoutFixtureId }) => cutoutFixtureId === TEMPERATURE_MODULE_V2_FIXTURE + ) + const magneticBlockFixtures = deckConfig.filter(({ cutoutFixtureId }) => + ([ + MAGNETIC_BLOCK_V1_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + ] as CutoutFixtureId[]).includes(cutoutFixtureId) + ) return ( @@ -102,15 +122,18 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { /> ))} - {stagingAreaFixtures.map(({ cutoutId }) => ( + {stagingAreaFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} - {emptyFixtures.map(({ cutoutId }) => ( + {emptyCutouts.map(({ cutoutId }) => ( ))} - {wasteChuteFixtures.map(({ cutoutId }) => ( + {wasteChuteFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} - {wasteChuteStagingAreaFixtures.map(({ cutoutId }) => ( + {wasteChuteStagingAreaFixtures.map(({ cutoutId, cutoutFixtureId }) => ( ))} - {trashBinFixtures.map(({ cutoutId }) => ( + {trashBinFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + ))} + {temperatureModuleFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {heaterShakerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {magneticBlockFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {thermocyclerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( + + ))} + {additionalStaticFixtures?.map(staticFixture => ( + ))} s.id === slotName ) if (slotDef == null) { @@ -52,7 +52,7 @@ export function LegacyDeckSlotLocation( const slotPosition = getPositionFromSlotId( slotName, - (ot2DeckDefV4 as unknown) as DeckDefinition + (ot2DeckDefV5 as unknown) as DeckDefinition ) const isFixedTrash = slotName === 'fixedTrash' diff --git a/components/src/icons/Icon.stories.tsx b/components/src/icons/Icon.stories.tsx index 1be5df8581cf..9d7b0f1141a6 100644 --- a/components/src/icons/Icon.stories.tsx +++ b/components/src/icons/Icon.stories.tsx @@ -1,33 +1,38 @@ import * as React from 'react' - -import { Box, SIZE_3 } from '@opentrons/components' +import { Flex } from '../primitives' +import { SPACING } from '../ui-style-constants' import { ICON_DATA_BY_NAME } from './icon-data' import { Icon as IconComponent } from './Icon' +import type { Meta, StoryObj } from '@storybook/react' -import type { Story, Meta } from '@storybook/react' - -export default { +const meta: Meta = { title: 'Library/Atoms/Icon', + component: IconComponent, argTypes: { name: { + options: Object.keys(ICON_DATA_BY_NAME), control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: 'alert', }, }, decorators: [ Story => ( - + - + ), ], -} as Meta +} -const Template: Story> = args => { - return +export default meta + +type Story = StoryObj + +export const Icon: Story = { + args: { + name: 'alert', + spin: false, + size: '4rem', + }, } -export const Icon = Template.bind({}) -Icon.args = { spin: false } diff --git a/components/src/instrument/InfoItem.tsx b/components/src/instrument/InfoItem.tsx deleted file mode 100644 index 82b5a491a379..000000000000 --- a/components/src/instrument/InfoItem.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react' - -import styles from './instrument.module.css' - -export interface InfoItemProps { - title: string | null - value: string - className?: string -} - -/** - * Used by `InstrumentInfo` for its titled values. - * But if you're using this, you probably want `LabeledValue` instead. - */ -export function InfoItem(props: InfoItemProps): JSX.Element { - const { title, value, className } = props - - return ( -
- {title != null ?

{title}

: null} - {value} -
- ) -} diff --git a/components/src/instrument/InstrumentInfo.tsx b/components/src/instrument/InstrumentInfo.tsx index d5d26a3b4b46..57ff12e0ed43 100644 --- a/components/src/instrument/InstrumentInfo.tsx +++ b/components/src/instrument/InstrumentInfo.tsx @@ -1,77 +1,103 @@ import * as React from 'react' import { LEFT, RIGHT } from '@opentrons/shared-data' -import { InfoItem } from './InfoItem' -import { InstrumentDiagram } from './InstrumentDiagram' -import styles from './instrument.module.css' import { Flex } from '../primitives' -import { SPACING } from '../ui-style-constants' +import { SPACING, TYPOGRAPHY } from '../ui-style-constants' +import { StyledText } from '../atoms' import { DIRECTION_COLUMN, JUSTIFY_CENTER } from '../styles' +import { InstrumentDiagram } from './InstrumentDiagram' import type { Mount } from '../robot-types' import type { InstrumentDiagramProps } from './InstrumentDiagram' +import styles from './instrument.module.css' + export interface InstrumentInfoProps { /** 'left' or 'right' */ mount: Mount - /** if true, show labels 'LEFT PIPETTE' / 'RIGHT PIPETTE' */ - showMountLabel?: boolean | null /** human-readable description, eg 'p300 Single-channel' */ description: string - /** paired tiprack models */ - tiprackModels?: string[] - /** if disabled, pipette & its info are grayed out */ - isDisabled: boolean /** specs of mounted pipette */ pipetteSpecs?: InstrumentDiagramProps['pipetteSpecs'] | null - /** classes to apply */ - className?: string - /** classes to apply to the info group child */ - infoClassName?: string + /** paired tiprack models */ + tiprackModels?: string[] /** children to display under the info */ children?: React.ReactNode + /** if true, show labels 'LEFT PIPETTE' / 'RIGHT PIPETTE' */ + showMountLabel?: boolean | null } +const MAX_WIDTH = '14rem' + export function InstrumentInfo(props: InstrumentInfoProps): JSX.Element { - const has96Channel = props.pipetteSpecs?.channels === 96 + const { + mount, + showMountLabel, + description, + tiprackModels, + pipetteSpecs, + children, + } = props + + const has96Channel = pipetteSpecs?.channels === 96 return ( - {props.mount === RIGHT && props.pipetteSpecs && ( + {mount === RIGHT && pipetteSpecs ? ( - )} + ) : null} + {/* NOTE: the color is our legacy c-font-dark, which matches the other colors in this component **/} + + + + {showMountLabel && !has96Channel ? `${mount} pipette` : 'pipette'} + + + {description} + + - - - {props.tiprackModels != null - ? props.tiprackModels.map((model, index) => ( - - )) - : null} + + + {'Tip rack'} + +
    + {tiprackModels != null && tiprackModels.length > 0 ? ( + tiprackModels.map((model, index) => ( +
  • + + {model} + +
  • + )) + ) : ( + + {'None'} + + )} +
+
- {props.children} - {props.mount === LEFT && props.pipetteSpecs && ( + {children} + {mount === LEFT && pipetteSpecs ? ( - )} + ) : null}
) } diff --git a/components/src/instrument/__tests__/InstrumentInfo.test.tsx b/components/src/instrument/__tests__/InstrumentInfo.test.tsx new file mode 100644 index 000000000000..bf92c48d4cb1 --- /dev/null +++ b/components/src/instrument/__tests__/InstrumentInfo.test.tsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, beforeEach, it, vi } from 'vitest' +import { LEFT, RIGHT, fixtureP1000SingleV2Specs } from '@opentrons/shared-data' +import { renderWithProviders } from '../../testing/utils' +import { InstrumentInfo } from '../InstrumentInfo' +import { InstrumentDiagram } from '../InstrumentDiagram' + +vi.mock('../InstrumentDiagram') +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} + +describe('InstrumentInfo', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + mount: LEFT, + description: 'mock description', + pipetteSpecs: fixtureP1000SingleV2Specs, + tiprackModels: ['mock1', 'mock2'], + showMountLabel: true, + } + vi.mocked(InstrumentDiagram).mockReturnValue( +
mock instrumentDiagram
+ ) + }) + it('renders a p1000 pipette with 2 tiprack models for left mount', () => { + render(props) + screen.getByText('mock instrumentDiagram') + screen.getByText('left pipette') + screen.getByText('mock description') + screen.getByText('Tip rack') + screen.getByText('mock1') + screen.getByText('mock2') + }) + it('renders a p1000 pipette with 1 tiprack model for right mount', () => { + props.mount = RIGHT + props.tiprackModels = ['mock1'] + render(props) + screen.getByText('mock instrumentDiagram') + screen.getByText('right pipette') + screen.getByText('mock description') + screen.getByText('Tip rack') + screen.getByText('mock1') + }) + it('renders none for pip and tiprack if none are selected', () => { + props.pipetteSpecs = undefined + props.tiprackModels = undefined + render(props) + screen.getByText('None') + }) +}) diff --git a/components/src/instrument/index.ts b/components/src/instrument/index.ts index 1153df43ae77..d566fb66e5be 100644 --- a/components/src/instrument/index.ts +++ b/components/src/instrument/index.ts @@ -1,4 +1,3 @@ -export * from './InfoItem' export * from './InstrumentDiagram' export * from './InstrumentGroup' export * from './InstrumentInfo' diff --git a/components/src/lists/TitledList.tsx b/components/src/lists/TitledList.tsx index 58a12d19b6eb..4fbe4ab58ee8 100644 --- a/components/src/lists/TitledList.tsx +++ b/components/src/lists/TitledList.tsx @@ -2,10 +2,13 @@ import * as React from 'react' import cx from 'classnames' -import styles from './lists.module.css' import { Icon } from '../icons' +import { StyledText } from '../atoms' +import { COLORS } from '../helix-design-system' import type { IconName, IconProps } from '../icons' +import styles from './lists.module.css' + // TODO(bc, 2021-03-31): reconsider whether this belongs in components library // it is bloated with application specific functionality @@ -98,6 +101,15 @@ export function TitledList(props: TitledListProps): JSX.Element { iconProps && iconProps.className ) + let textColor = '' + if (disabled) { + // the below hex code is for our legacy --c-font-disabled to match other text colors + textColor = '#9c9c9c' + } else if (props.selected && !disabled) { + // the below hex code is for our legacy --c-highlight to match other text colors + textColor = '#5fd8ee' + } + return (
)} -

+ {props.title} -

+ {collapsible && (
= { + title: 'Library/Molecules/LocationIcon', argTypes: { iconName: { control: { type: 'select', - options: Object.keys(ICON_DATA_BY_NAME), }, - defaultValue: undefined, + options: Object.keys(ICON_DATA_BY_NAME), }, slotName: { control: { type: 'select', - options: slots, }, - defaultValue: undefined, + options: slots, }, }, component: LocationIcon, - // Note (kk:08/29/2023) this component is located in components so avoid importing const from app parameters: { viewport: { viewports: customViewports, @@ -56,26 +51,25 @@ export default { }, decorators: [ Story => ( - <> + - + ), ], -} as Meta - -const Template: Story> = args => ( - - - -) +} +export default meta +type Story = StoryObj -export const DisplaySlot = Template.bind({}) -DisplaySlot.args = { - slotName: 'A1', +export const DisplaySlot: Story = { + args: { + slotName: 'A1', + iconName: undefined, + }, } -export const DisplayIcon = Template.bind({}) -DisplayIcon.args = { - iconName: 'ot-temperature-v2', +export const DisplayIcon: Story = { + args: { + iconName: 'ot-temperature-v2', + }, } diff --git a/components/src/molecules/ParametersTable/NoParameters.tsx b/components/src/molecules/ParametersTable/InfoScreen.tsx similarity index 70% rename from components/src/molecules/ParametersTable/NoParameters.tsx rename to components/src/molecules/ParametersTable/InfoScreen.tsx index b0afb82530f8..b9798f828e33 100644 --- a/components/src/molecules/ParametersTable/NoParameters.tsx +++ b/components/src/molecules/ParametersTable/InfoScreen.tsx @@ -7,10 +7,15 @@ import { Icon } from '../../icons' import { Flex } from '../../primitives' import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' -interface NoParametersProps { - t?: any +interface InfoScreenProps { + contentType: 'parameters' | 'moduleControls' } -export function NoParameters({ t }: NoParametersProps): JSX.Element { + +export function InfoScreen({ contentType }: InfoScreenProps): JSX.Element { + const bodyText = + contentType === 'parameters' + ? 'No parameters specified in this protocol' + : 'Connect modules to see controls' return ( - {t != null - ? t('no_parameters') - : 'No parameters specified in this protocol'} + {bodyText} ) diff --git a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx index ce55f700dc3b..d68e2f80a959 100644 --- a/components/src/molecules/ParametersTable/ParametersTable.stories.tsx +++ b/components/src/molecules/ParametersTable/ParametersTable.stories.tsx @@ -1,15 +1,10 @@ -import * as React from 'react' -import { ParametersTable } from '@opentrons/components' -import type { Story, Meta } from '@storybook/react' -import type { RunTimeParameter } from '@opentrons/shared-data' - -export default { - title: 'Library/Molecules/ParametersTable', -} as Meta +import * as React from 'react-remove-scroll' +import { Flex } from '../../primitives' +import { SPACING } from '../../ui-style-constants' +import { ParametersTable } from './index' -const Template: Story> = args => ( - -) +import type { Meta, StoryObj } from '@storybook/react' +import type { RunTimeParameter } from '@opentrons/shared-data' const runTimeParameters: RunTimeParameter[] = [ { @@ -17,7 +12,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, }, { @@ -25,7 +20,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Use Gripper', variableName: 'USE_GRIPPER', description: 'For using the gripper.', - type: 'boolean', + type: 'bool', default: true, }, { @@ -34,7 +29,7 @@ const runTimeParameters: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, }, { @@ -42,7 +37,7 @@ const runTimeParameters: RunTimeParameter[] = [ displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, }, { @@ -153,7 +148,24 @@ const runTimeParameters: RunTimeParameter[] = [ default: 'flex', }, ] -export const Default = Template.bind({}) -Default.args = { - runTimeParameters: runTimeParameters, + +const meta: Meta = { + title: 'Library/Molecules/ParametersTable', + component: ParametersTable, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta + +type Story = StoryObj + +export const DefaultParameterTable: Story = { + args: { + runTimeParameters: runTimeParameters, + }, } diff --git a/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx new file mode 100644 index 000000000000..a6f3b78a3587 --- /dev/null +++ b/components/src/molecules/ParametersTable/__tests__/InfoScreen.test.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../testing/utils' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { InfoScreen } from '../InfoScreen' + +const render = (props: React.ComponentProps) => { + return renderWithProviders() +} + +describe('InfoScreen', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + contentType: 'parameters', + } + }) + + it('should render text and icon with proper color - parameters', () => { + render(props) + screen.getByLabelText('alert') + screen.getByText('No parameters specified in this protocol') + }) + + it('should render text and icon with proper color - module controls', () => { + props = { + contentType: 'moduleControls', + } + render(props) + screen.getByLabelText('alert') + screen.getByText('Connect modules to see controls') + }) + + it('should have proper styles', () => { + render(props) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `background-color: ${COLORS.grey30}` + ) + expect(screen.getByTestId('InfoScreen_parameters')).toHaveStyle( + `border-radius: ${BORDERS.borderRadius8}` + ) + expect(screen.getByLabelText('alert')).toHaveStyle( + `color: ${COLORS.grey60}` + ) + }) +}) diff --git a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx b/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx deleted file mode 100644 index 5b2e7f2927d5..000000000000 --- a/components/src/molecules/ParametersTable/__tests__/NoParameters.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' - -import { renderWithProviders } from '../../../testing/utils' -import { BORDERS, COLORS } from '../../../helix-design-system' -import { NoParameters } from '../NoParameters' - -const render = (props: React.ComponentProps) => { - return renderWithProviders() -} - -const tMock = (key: string) => key - -describe('NoParameters', () => { - it('should render text and icon with proper color', () => { - render({}) - screen.getByLabelText('alert') - screen.getByText('No parameters specified in this protocol') - }) - - it('should have proper styles', () => { - render({}) - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `background-color: ${COLORS.grey30}` - ) - expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( - `border-radius: ${BORDERS.borderRadius8}` - ) - expect(screen.getByLabelText('alert')).toHaveStyle( - `color: ${COLORS.grey60}` - ) - }) - - it('should render the raw i18n value if a t is provided', () => { - render({ - t: tMock, - }) - screen.getByText('no_parameters') - }) -}) diff --git a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx index 1c9cd2d571ca..5cd4b59a59b0 100644 --- a/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx +++ b/components/src/molecules/ParametersTable/__tests__/ParametersTable.test.tsx @@ -13,7 +13,7 @@ const mockRunTimeParameter: RunTimeParameter[] = [ variableName: 'TIP_TRASH', description: 'to throw tip into the trash or to not throw tip into the trash', - type: 'boolean', + type: 'bool', default: true, value: true, }, @@ -74,7 +74,7 @@ const render = (props: React.ComponentProps) => { return renderWithProviders() } -describe('ParametersTabl', () => { +describe('ParametersTable', () => { let props: React.ComponentProps beforeEach(() => { @@ -98,12 +98,14 @@ describe('ParametersTabl', () => { screen.getByText('EtoH Volume') screen.getByText('6.5 mL') - screen.getByText('1.5-10') + screen.getByText('1.5-10.0') + // more than 2 options screen.getByText('Default Module Offsets') screen.getByText('No offsets') - screen.getByText('3 choices') + screen.getByText('3 options') + // 2 options screen.getByText('pipette mount') screen.getByText('Left') screen.getByText('Left, Right') @@ -116,4 +118,9 @@ describe('ParametersTabl', () => { screen.getByText('default_value') screen.getByText('range') }) + + it('should render a description icon if description is provided', () => { + render(props) + screen.getByTestId('Icon_0') + }) }) diff --git a/components/src/molecules/ParametersTable/index.tsx b/components/src/molecules/ParametersTable/index.tsx index 4ff5cdeeb18a..5ae0d36d550a 100644 --- a/components/src/molecules/ParametersTable/index.tsx +++ b/components/src/molecules/ParametersTable/index.tsx @@ -1,9 +1,17 @@ import * as React from 'react' -import styled from 'styled-components' -import { formatRunTimeParameterValue } from '@opentrons/shared-data' -import { BORDERS } from '../../helix-design-system' +import styled, { css } from 'styled-components' +import { + formatRunTimeParameterDefaultValue, + formatRunTimeParameterMinMax, + orderRuntimeParameterRangeOptions, +} from '@opentrons/shared-data' +import { BORDERS, COLORS } from '../../helix-design-system' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' import { StyledText } from '../../atoms/StyledText' +import { Tooltip, useHoverTooltip } from '../../tooltips' +import { Icon } from '../../icons' +import { Flex } from '../../primitives' +import { DISPLAY_INLINE } from '../../styles' import type { RunTimeParameter } from '@opentrons/shared-data' @@ -19,29 +27,30 @@ export function ParametersTable({ runTimeParameters, t, }: ProtocolParameterItemsProps): JSX.Element { - const formatRange = ( - runTimeParameter: RunTimeParameter, - minMax: string - ): string => { + const formatRange = (runTimeParameter: RunTimeParameter): string => { const { type } = runTimeParameter + const minMax = formatRunTimeParameterMinMax(runTimeParameter) const choices = 'choices' in runTimeParameter ? runTimeParameter.choices : [] const count = choices.length + if (count > 0) { + return count > 2 + ? t != null + ? t('num_options', { num: count }) + : `${count} options` + : orderRuntimeParameterRangeOptions(choices) + } + switch (type) { case 'int': case 'float': return minMax - case 'boolean': + case 'bool': return t != null ? t('on_off') : 'On, off' - case 'str': - if (count > 2) { - return t != null ? t('choices', { count }) : `${count} choices` - } else { - return choices.map(choice => choice.displayName).join(', ') - } + default: + return '' } - return '' } return ( @@ -57,25 +66,27 @@ export function ParametersTable({ {runTimeParameters.map((parameter: RunTimeParameter, index: number) => { - const min = 'min' in parameter ? parameter.min : 0 - const max = 'max' in parameter ? parameter.max : 0 return ( - - {parameter.displayName} - + - {formatRunTimeParameterValue(parameter, t)} + {formatRunTimeParameterDefaultValue(parameter, t)} - - - {formatRange(parameter, `${min}-${max}`)} - + + {formatRange(parameter)} ) @@ -85,6 +96,52 @@ export function ParametersTable({ ) } +interface ParameterNameProps { + displayName: string + description: string | null + isLast: boolean + index: number +} + +const ParameterName = (props: ParameterNameProps): JSX.Element => { + const { displayName, description, isLast, index } = props + const [targetProps, tooltipProps] = useHoverTooltip() + + return ( + + + {displayName} + + {description != null ? ( + <> + + + + + {description} + + + ) : null} + + ) +} + const StyledTable = styled.table` width: 100%; border-collapse: collapse; @@ -93,7 +150,8 @@ const StyledTable = styled.table` const StyledTableHeader = styled.th` ${TYPOGRAPHY.labelSemiBold} - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; + padding-bottom: ${SPACING.spacing8}; border-bottom: ${BORDERS.lineBorder}; ` @@ -102,16 +160,21 @@ interface StyledTableRowProps { } const StyledTableRow = styled.tr` - padding: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing16}; border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; ` interface StyledTableCellProps { isLast: boolean + paddingRight?: string + display?: string } const StyledTableCell = styled.td` - padding-left: ${SPACING.spacing8}; + width: 33%; + display: ${props => (props.display != null ? props.display : 'table-cell')}; padding-top: ${SPACING.spacing12}; padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; + padding-right: ${props => + props.paddingRight != null ? props.paddingRight : SPACING.spacing16}; ` diff --git a/components/src/molecules/index.tsx b/components/src/molecules/index.ts similarity index 66% rename from components/src/molecules/index.tsx rename to components/src/molecules/index.ts index 3231c2f93a97..cc7a1eacdbd3 100644 --- a/components/src/molecules/index.tsx +++ b/components/src/molecules/index.ts @@ -1,4 +1,4 @@ export * from './LocationIcon' export * from './RoundTab' export * from './ParametersTable' -export * from './ParametersTable/NoParameters' +export * from './ParametersTable/InfoScreen' diff --git a/components/src/primitives/Box.stories.tsx b/components/src/primitives/Box.stories.tsx index 3d322842a0a3..54fd773d1250 100644 --- a/components/src/primitives/Box.stories.tsx +++ b/components/src/primitives/Box.stories.tsx @@ -1,21 +1,25 @@ -import * as React from 'react' +import { COLORS, BORDERS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' import { Box as BoxComponent } from './Box' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Box', -} as Meta + component: BoxComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Box = Template.bind({}) -Box.args = { - children: - 'This is a simple box atom that accepts all primitive styling props.', - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', +export const Box: Story = { + args: { + children: + 'This is a simple box atom that accepts all primitive styling props.', + backgroundColor: COLORS.grey60, + border: `1px ${BORDERS.styleSolid} black`, + padding: SPACING.spacing16, + maxWidth: '20rem', + }, } diff --git a/components/src/primitives/Flex.stories.tsx b/components/src/primitives/Flex.stories.tsx index f9773b5fc55a..1335fa52919e 100644 --- a/components/src/primitives/Flex.stories.tsx +++ b/components/src/primitives/Flex.stories.tsx @@ -1,35 +1,51 @@ import * as React from 'react' -import { Flex as FlexComponent } from './Flex' -import { - Box, - DIRECTION_COLUMN, - JUSTIFY_SPACE_AROUND, -} from '@opentrons/components' +import { BORDERS, COLORS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' +import { DIRECTION_COLUMN, JUSTIFY_SPACE_AROUND } from '../styles' +import { StyledText } from '../atoms/StyledText' +import { Box, Flex as FlexComponent } from '../primitives' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Flex', -} as Meta + component: FlexComponent, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Flex = Template.bind({}) -Flex.args = { - children: [ - - This is a flex child - , - - This is a flex child - , - ], - flexDirection: DIRECTION_COLUMN, - justifyContent: JUSTIFY_SPACE_AROUND, - backgroundColor: 'grey', - border: '1px solid black', - padding: '1rem', - maxWidth: '20rem', - height: '10rem', +export const Flex: Story = { + args: { + children: [ + + + This is a flex child + + , + + + This is a flex child + + , + ], + flexDirection: DIRECTION_COLUMN, + justifyContent: JUSTIFY_SPACE_AROUND, + backgroundColor: 'grey', + border: '1px solid black', + padding: '1rem', + maxWidth: '20rem', + height: '10rem', + }, } diff --git a/components/src/primitives/Link.stories.tsx b/components/src/primitives/Link.stories.tsx index 2f54b4729207..1aa3890d2932 100644 --- a/components/src/primitives/Link.stories.tsx +++ b/components/src/primitives/Link.stories.tsx @@ -1,24 +1,27 @@ -import * as React from 'react' import { Link } from './Link' -import type { Story, Meta } from '@storybook/react' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const meta: Meta = { title: 'Library/Atoms/Link', -} as Meta + component: Link, +} + +export default meta + +type Story = StoryObj -const Template: Story> = args => ( - -) -export const Basic = Template.bind({}) -Basic.args = { - children: 'hello anchor', - href: '#', +export const Basic: Story = { + args: { + children: 'hello anchor', + href: '#', + }, } -export const External = Template.bind({}) -External.args = { - children: 'hello opentrons', - external: true, - href: 'https://www.opentrons.com', +export const External: Story = { + args: { + children: 'hello opentrons', + external: true, + href: 'https://www.opentrons.com', + }, } diff --git a/components/src/styles/flexbox.ts b/components/src/styles/flexbox.ts index bc588372e96a..2c36936b2002 100644 --- a/components/src/styles/flexbox.ts +++ b/components/src/styles/flexbox.ts @@ -1,6 +1,7 @@ export const FLEX_NONE = 'none' export const FLEX_AUTO = 'auto' export const FLEX_MIN_CONTENT = 'min-content' +export const FLEX_MAX_CONTENT = 'max-content' export const ALIGN_NORMAL = 'normal' export const ALIGN_START = 'start' diff --git a/components/webpack.config.js b/components/webpack.config.js deleted file mode 100644 index 648eaee3432d..000000000000 --- a/components/webpack.config.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict' - -const path = require('path') -const { rules } = require('@opentrons/webpack-config') - -const ENTRY_INDEX = path.join(__dirname, 'src/barrel.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') - -module.exports = { - target: 'web', - entry: { index: ENTRY_INDEX }, - output: { - path: OUTPUT_PATH, - filename: 'opentrons-components.js', - library: '@opentrons/components', - libraryTarget: 'umd', - globalObject: 'this', - }, - mode: 'production', - module: { rules: [rules.js] }, - resolve: { - extensions: ['.wasm', '.mjs', '.js', '.ts', '.tsx', '.json'], - }, - externals: { - react: { - root: 'React', - commonjs2: 'react', - commonjs: 'react', - amd: 'react', - }, - 'react-dom': { - root: 'ReactDOM', - commonjs2: 'react-dom', - commonjs: 'react-dom', - amd: 'react-dom', - }, - }, -} diff --git a/discovery-client/vite.config.ts b/discovery-client/vite.config.ts index 7cbd9ae43c3d..c67977a83597 100644 --- a/discovery-client/vite.config.ts +++ b/discovery-client/vite.config.ts @@ -1,13 +1,14 @@ -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' import pkg from './package.json' import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import type { UserConfig } from 'vite' export default defineConfig( async (): Promise => { diff --git a/discovery-client/webpack.config.js b/discovery-client/webpack.config.js deleted file mode 100644 index c15a3bae1c2a..000000000000 --- a/discovery-client/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { DefinePlugin } = require('webpack') -const { nodeBaseConfig } = require('@opentrons/webpack-config') -const { versionForProject } = require('../scripts/git-version') - -const ENTRY_INDEX = path.join(__dirname, 'src/index.ts') -const ENTRY_CLI = path.join(__dirname, 'src/cli.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') -const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' - -module.exports = async () => - webpackMerge(nodeBaseConfig, { - entry: { - index: ENTRY_INDEX, - cli: ENTRY_CLI, - }, - output: { path: OUTPUT_PATH }, - plugins: [ - new DefinePlugin({ - _PKG_VERSION_: JSON.stringify(await versionForProject(project)), - }), - ], - }) diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile index 5e6d7264113c..a48b794977fa 100755 --- a/hardware-testing/Makefile +++ b/hardware-testing/Makefile @@ -79,36 +79,26 @@ sdist: .PHONY: test test: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-cov test-cov: - -$(MAKE) apply-patches-gravimetric $(pytest) $(tests) $(test_opts) $(cov_opts) - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-single test-photometric-single: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --photoplate-col-offset 3 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 1 --tip 50 --dye-well-col-offset 3 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric-multi test-photometric-multi: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 50 --channels 8 --tip 50 - -$(MAKE) remove-patches-gravimetric .PHONY: test-photometric test-photometric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 50 --trials 1 $(python) -m hardware_testing.gravimetric --photometric --simulate --pipette 1000 --channels 96 --tip 200 --trials 1 - -$(MAKE) remove-patches-gravimetric .PHONY: test-gravimetric-single test-gravimetric-single: @@ -134,14 +124,12 @@ test-gravimetric-96: .PHONY: test-gravimetric test-gravimetric: - -$(MAKE) apply-patches-gravimetric $(python) -m hardware_testing.gravimetric.daily_setup --simulate $(python) -m hardware_testing.gravimetric.daily_setup --simulate --calibrate $(MAKE) test-gravimetric-single $(MAKE) test-gravimetric-multi $(MAKE) test-gravimetric-96 $(MAKE) test-photometric - -$(MAKE) remove-patches-gravimetric .PHONY: test-production-qc test-production-qc: @@ -167,16 +155,22 @@ test-examples: test-scripts: $(python) -m hardware_testing.scripts.bowtie_ot3 --simulate +.PHONY: test-liquid-sense +test-liquid-sense: + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 1 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 50 --channels 8 + $(python) -m hardware_testing.liquid_sense --simulate --pipette 1000 --channels 96 + .PHONY: test-integration test-integration: test-production-qc test-examples test-scripts test-gravimetric .PHONY: lint lint: - -$(MAKE) apply-patches-gravimetric $(python) -m mypy hardware_testing tests $(python) -m black --check hardware_testing tests setup.py $(python) -m flake8 hardware_testing tests setup.py - -$(MAKE) remove-patches-gravimetric .PHONY: format format: @@ -297,9 +291,11 @@ sync-ot3: sync-sw-ot3 sync-fw-ot3 .PHONY: push-ot3-gravimetric push-ot3-gravimetric: + $(MAKE) push-ot3 + ssh $(ssh_helper_ot3) root@$(host) "mkdir -p /data/labware/v2/custom_definitions/custom_beta" + scp $(ssh_helper_ot3) -r hardware_testing/labware/* root@$(host):/data/labware/v2/custom_definitions/custom_beta/ $(MAKE) apply-patches-gravimetric - -$(MAKE) sync-sw-ot3 - scp $(ssh_helper_ot3) -r hardware_testing/labware root@$(host):/data/labware/v2/custom_definitions/custom_beta/ + cd ../ && $(MAKE) -C shared-data push-ot3 $(MAKE) remove-patches-gravimetric .PHONY: apply-patches-gravimetric diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index 350741ebc792..00b73893e6d0 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -92,7 +92,7 @@ def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase ui.print_info(f"Trying to connect to env sensor on port {port}") sensor = AsairSensor.connect(port) ser_id = sensor.get_serial() - if len(ser_id) != 0: + if ser_id == " ": ui.print_info(f"Found env sensor {ser_id} on port {port}") return sensor except: # noqa: E722 diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index 54a8278adefc..0855345598be 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -2,10 +2,8 @@ from json import load as json_load from pathlib import Path import argparse -from time import time from typing import List, Union, Dict, Optional, Any, Tuple from dataclasses import dataclass -from opentrons.hardware_control.types import OT3Mount from opentrons.protocol_api import ProtocolContext from . import report import subprocess @@ -42,16 +40,15 @@ from .measurement.record import GravimetricRecorder from .measurement import DELAY_FOR_MEASUREMENT from .measurement.scale import Scale -from .measurement.environment import read_environment_data from .trial import TestResources, _change_pipettes from .tips import get_tips from hardware_testing.drivers import asair_sensor from opentrons.protocol_api import InstrumentContext +from opentrons.protocol_engine.types import LabwareOffset -# FIXME: bump to v2.15 to utilize protocol engine -API_LEVEL = "2.13" +API_LEVEL = "2.18" -LABWARE_OFFSETS: List[dict] = [] +LABWARE_OFFSETS: List[LabwareOffset] = [] # Keyed by pipette volume, channel count, and tip volume in that order GRAVIMETRIC_CFG = { @@ -90,6 +87,19 @@ }, } +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + PHOTOMETRIC_CFG = { 50: { 1: { @@ -148,22 +158,18 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: ui.print_info( "Starting opentrons-robot-server, so we can http GET labware offsets" ) - offsets = workarounds.http_get_all_labware_offsets() - ui.print_info(f"found {len(offsets)} offsets:") - for offset in offsets: - ui.print_info(f"\t{offset['createdAt']}:") - ui.print_info(f"\t\t{offset['definitionUri']}") - ui.print_info(f"\t\t{offset['vector']}") - LABWARE_OFFSETS.append(offset) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") # gather the custom labware (for simulation) custom_defs = {} if args.simulate: labware_dir = Path(__file__).parent.parent / "labware" custom_def_uris = [ "radwag_pipette_calibration_vial", - "opentrons_flex_96_tiprack_50ul_adp", - "opentrons_flex_96_tiprack_200ul_adp", - "opentrons_flex_96_tiprack_1000ul_adp", ] for def_uri in custom_def_uris: with open(labware_dir / def_uri / "1.json", "r") as f: @@ -172,9 +178,12 @@ def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: _ctx = helpers.get_api_context( API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, - deck_version="2", + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], extra_labware=custom_defs, ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) return _ctx @classmethod @@ -301,7 +310,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 trials=trials, name=name, robot_serial=robot_serial, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) else: if args.increment: @@ -334,7 +343,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": # noqa: C901 name=name, environment_sensor=environment_sensor, trials=trials, - fw_version=_ctx._core.get_hardware().fw_version, + fw_version=workarounds.get_sync_hw_api(_ctx).fw_version, ) return RunArgs( @@ -387,7 +396,6 @@ def build_gravimetric_cfg( pipette_channels=run_args.pipette_channels, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, labware_on_scale=run_args.protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] slot_scale=run_args.protocol_cfg.SLOT_SCALE, # type: ignore[attr-defined] slots_tiprack=run_args.protocol_cfg.SLOTS_TIPRACK[tip_volume], # type: ignore[attr-defined] @@ -436,7 +444,6 @@ def build_photometric_cfg( increment=False, tip_volume=tip_volume, trials=run_args.trials, - labware_offsets=LABWARE_OFFSETS, photoplate=run_args.protocol_cfg.PHOTOPLATE_LABWARE, # type: ignore[attr-defined] photoplate_slot=run_args.protocol_cfg.SLOT_PLATE, # type: ignore[attr-defined] reservoir=run_args.protocol_cfg.RESERVOIR_LABWARE, # type: ignore[attr-defined] @@ -569,7 +576,6 @@ def _main( parser.add_argument( "--mode", type=str, choices=["", "default", "lowVolumeDefault"], default="" ) - parser.add_argument("--pre-heat", action="store_true") args = parser.parse_args() run_args = RunArgs.build_run_args(args) if not run_args.ctx.is_simulating(): @@ -580,48 +586,13 @@ def _main( shell=True, ) sleep(1) - hw = run_args.ctx._core.get_hardware() + hw = workarounds.get_sync_hw_api(run_args.ctx) try: if not run_args.ctx.is_simulating() and not args.photometric: ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") ui.print_info("homing...") run_args.ctx.home() - if args.pre_heat: - ui.print_header("PRE-HEAT") - mnt = OT3Mount.LEFT - hw.add_tip(mnt, 1) - hw.prepare_for_aspirate(mnt) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - start_temp = env_data.celsius_pipette - temp_limit = min(start_temp + 3.0, 28.0) - max_pre_heat_seconds = 60 * 10 - now = time() - start_time = now - while ( - now - start_time < max_pre_heat_seconds - and env_data.celsius_pipette < temp_limit - ): - ui.print_info( - f"pre-heat {int(now - start_time)} seconds " - f"({max_pre_heat_seconds} limit): " - f"{round(env_data.celsius_pipette, 2)} C " - f"({round(temp_limit, 2)} C limit)" - ) - # NOTE: moving slowly helps make sure full current is sent to coils - hw.aspirate(mnt, rate=0.1) - hw.dispense(mnt, rate=0.1, push_out=0) - env_data = read_environment_data( - mnt.name.lower(), hw.is_simulator, run_args.environment_sensor - ) - if run_args.ctx.is_simulating(): - now += 1 - else: - now = time() - hw.remove_tip(mnt) - for tip, volumes in run_args.volumes: if args.channels == 96 and not run_args.ctx.is_simulating(): ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) @@ -634,5 +605,5 @@ def _main( _change_pipettes(run_args.ctx, run_args.pipette) if not run_args.ctx.is_simulating(): serial_logger.terminate() - del hw._backend.eeprom_driver._gpio + del hw._backend.eeprom_driver._gpio # still need this? print("done\n\n") diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index 3af376a04cff..993e8716a924 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -24,7 +24,6 @@ class VolumetricConfig: pipette_mount: str tip_volume: int trials: int - labware_offsets: List[dict] slots_tiprack: List[int] increment: bool return_tip: bool @@ -194,11 +193,11 @@ def _get_liquid_probe_settings( plunger_speed=lqid_cfg["plunger_speed"], sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], expected_liquid_height=110, - output_option=OutputOptions.stream_to_csv, + output_option=OutputOptions.sync_only, aspirate_while_sensing=False, auto_zero_sensor=True, num_baseline_reads=10, - data_file="/var/pressure_sensor_data.csv", + data_file="/data/testing_data/pressure.csv", ) diff --git a/hardware-testing/hardware_testing/gravimetric/daily_setup.py b/hardware-testing/hardware_testing/gravimetric/daily_setup.py index bc13dc9d0bf5..77569b43c110 100644 --- a/hardware-testing/hardware_testing/gravimetric/daily_setup.py +++ b/hardware-testing/hardware_testing/gravimetric/daily_setup.py @@ -13,8 +13,9 @@ ) from hardware_testing.gravimetric.config import GANTRY_MAX_SPEED from hardware_testing.gravimetric.measurement.scale import Scale # type: ignore[import] -from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.gravimetric import helpers from hardware_testing.gravimetric.__main__ import API_LEVEL +from hardware_testing.gravimetric.workarounds import get_sync_hw_api TEST_NAME = "gravimetric-daily-setup" @@ -253,7 +254,7 @@ def _calibrate() -> None: API_LEVEL, # type: ignore[attr-defined] is_simulating=args.simulate, ) - _hw = workarounds.get_sync_hw_api(_ctx) + _hw = get_sync_hw_api(_ctx) _hw.set_status_bar_state(COLOR_STATES["idle"]) _rec = GravimetricRecorder( GravimetricRecorderConfig( diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index cf2b8fb1ecc1..76b8ff037e23 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -18,7 +18,6 @@ _calculate_average, _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, ) @@ -53,6 +52,7 @@ import glob from opentrons.hardware_control.types import StatusBarState +from hardware_testing.gravimetric.workarounds import get_sync_hw_api _MEASUREMENTS: List[Tuple[str, MeasurementData]] = list() @@ -89,7 +89,7 @@ def _generate_callbacks_for_trial( if blank_measurement: volume = None - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT pip_ax = Axis.of_main_tool_actuator(hw_mount) estimate_bottom: float = -1 @@ -179,7 +179,6 @@ def _load_labware(ctx: ProtocolContext, cfg: config.GravimetricConfig) -> Labwar labware_on_scale = ctx.load_labware( cfg.labware_on_scale, location=cfg.slot_scale, namespace=namespace ) - _apply_labware_offsets(cfg, [labware_on_scale]) return labware_on_scale @@ -283,9 +282,13 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: m_tag = _tag(m_type) if trial.recorder.is_simulator and not trial.blank: if m_type == MeasurementType.ASPIRATE: - trial.recorder.add_simulation_mass(trial.volume * -0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * -0.001 + ) elif m_type == MeasurementType.DISPENSE: - trial.recorder.add_simulation_mass(trial.volume * 0.001) + trial.recorder.add_simulation_mass( + trial.channel_count * trial.volume * 0.001 + ) m_data = record_measurement_data( trial.ctx, m_tag, @@ -327,8 +330,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: else: # center channel over well trial.pipette.move_to(trial.well.top(50).move(trial.channel_offset)) - mnt = OT3Mount.RIGHT if trial.pipette.mount == "right" else OT3Mount.LEFT - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry m_data_init = _record_measurement_and_store(MeasurementType.INIT) ui.print_info(f"\tinitial grams: {m_data_init.grams_average} g") # update the vials volumes, using the last-known weight @@ -357,7 +359,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "aspirate") m_data_aspirate = _record_measurement_and_store(MeasurementType.ASPIRATE) @@ -379,7 +381,7 @@ def _record_measurement_and_store(m_type: MeasurementType) -> MeasurementData: mode=trial.mode, clear_accuracy_function=trial.cfg.increment, ) - trial.ctx._core.get_hardware().retract(mnt) # retract to top of gantry + trial.pipette._retract() # retract to top of gantry _take_photos(trial, "dispense") m_data_dispense = _record_measurement_and_store(MeasurementType.DISPENSE) ui.print_info(f"\tgrams after dispense: {m_data_dispense.grams_average} g") @@ -500,8 +502,7 @@ def _calculate_evaporation( resources.env_sensor, ) ui.print_info(f"running {config.NUM_BLANK_TRIALS}x blank measurements") - mnt = OT3Mount.RIGHT if resources.pipette.mount == "right" else OT3Mount.LEFT - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() for i in range(config.SCALE_SECONDS_TO_TRUE_STABILIZE): ui.print_info( f"wait for scale to stabilize " @@ -545,7 +546,7 @@ def _get_liquid_height( if not resources.ctx.is_simulating() and not cfg.same_tip: ui.alert_user_ready( f"Please replace the {cfg.tip_volume}ul tips in slot 2", - resources.ctx._core.get_hardware(), + get_sync_hw_api(resources.ctx), ) _tip_counter[0] = 0 if cfg.jog: @@ -595,7 +596,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq recorder._recording = GravimetricRecording() report.store_config_gm(resources.test_report, cfg) calibration_tip_in_use = True - hw_api = resources.ctx._core.get_hardware() + hw_api = get_sync_hw_api(resources.ctx) if resources.ctx.is_simulating(): _PREV_TRIAL_GRAMS = None _MEASUREMENTS = list() @@ -605,8 +606,6 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq setup_channel_offset = _get_channel_offset(cfg, channel=0) first_tip_location = first_tip.top().move(setup_channel_offset) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=first_tip_location) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) ui.print_info("moving to scale") well = labware_on_scale["A1"] _liquid_height = _get_liquid_height(resources, cfg, well) @@ -642,6 +641,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq resources.pipette, return_tip=False, minimum_z_height=_minimum_z_height(cfg), + offset=_get_channel_offset(cfg, 0), ) # always trash calibration tips calibration_tip_in_use = False trial_count = 0 @@ -662,7 +662,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq actual_asp_list_all = [] actual_disp_list_all = [] ui.print_title(f"{volume} uL") - + resources.pipette.configure_for_volume(volume) trial_asp_dict: Dict[int, List[float]] = { trial: [] for trial in range(cfg.trials) } @@ -694,12 +694,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq cfg, location=next_tip_location, ) - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry ( actual_aspirate, aspirate_data, @@ -742,14 +737,12 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq ) ui.print_info("dropping tip") if not cfg.same_tip: - mnt = ( - OT3Mount.LEFT - if cfg.pipette_mount == "left" - else OT3Mount.RIGHT - ) - resources.ctx._core.get_hardware().retract(mnt) + resources.pipette._retract() # retract to top of gantry _drop_tip( - resources.pipette, cfg.return_tip, _minimum_z_height(cfg) + resources.pipette, + cfg.return_tip, + _minimum_z_height(cfg), + _get_channel_offset(cfg, run_trial.channel), ) ui.print_header(f"{volume} uL channel {channel + 1} CALCULATIONS") @@ -809,7 +802,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq acceptable_d = trials[volume][channel][0].acceptable_d print(f"acceptable cv {acceptable_cv} acceptable_d {acceptable_d}") print(f"dispense cv {dispense_cv} aspirate_cv {aspirate_cv}") - print(f"dispense d {dispense_cv} aspirate_d {aspirate_d}") + print(f"dispense d {dispense_d} aspirate_d {aspirate_d}") if ( not cfg.ignore_fail and acceptable_cv is not None @@ -820,8 +813,8 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq if ( dispense_cv > acceptable_cv or aspirate_cv > acceptable_cv - or aspirate_d > acceptable_d - or dispense_d > acceptable_d + or abs(aspirate_d) > acceptable_d + or abs(dispense_d) > acceptable_d ): raise RuntimeError( f"Trial with volume {volume} on channel {channel} did not pass spec" diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index 5b36acc46f38..217109dd89d8 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -5,7 +5,7 @@ from opentrons.protocol_api import ProtocolContext, Well, Labware from hardware_testing.data import ui -from hardware_testing.opentrons_api.types import Point, OT3Mount +from hardware_testing.opentrons_api.types import Point from .measurement import ( MeasurementType, create_measurement_tag, @@ -18,7 +18,6 @@ from .helpers import ( _jog_to_find_liquid_height, _sense_liquid_height, - _apply_labware_offsets, _pick_up_tip, _drop_tip, get_list_of_wells_affected, @@ -110,7 +109,6 @@ def _load_labware( photoplate = loaded_labwares[cfg.photoplate_slot] else: photoplate = ctx.load_labware(cfg.photoplate, location=cfg.photoplate_slot) - _apply_labware_offsets(cfg, [photoplate]) if ( cfg.reservoir_slot in loaded_labwares.keys() @@ -119,7 +117,6 @@ def _load_labware( reservoir = loaded_labwares[cfg.reservoir_slot] else: reservoir = ctx.load_labware(cfg.reservoir, location=cfg.reservoir_slot) - _apply_labware_offsets(cfg, [reservoir]) return photoplate, reservoir @@ -218,11 +215,10 @@ def _record_measurement_and_store(m_type: MeasurementType) -> EnvironmentData: touch_tip=trial.cfg.touch_tip, ) _record_measurement_and_store(MeasurementType.DISPENSE) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) + trial.pipette._retract() # retract to top of gantry if (i + 1) == num_dispenses: if not trial.cfg.same_tip: _drop_tip(trial.pipette, trial.cfg.return_tip) - trial.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not trial.ctx.is_simulating() and trial.channel_count == 96: ui.get_user_ready("add SEAL to plate and remove from DECK") return @@ -350,13 +346,13 @@ def _find_liquid_height( setup_tip = _next_tip(resources, cfg, cfg.pipette_channels == 1) volume_for_setup = max(resources.test_volumes) _pick_up_tip(resources.ctx, resources.pipette, cfg, location=setup_tip.top()) - mnt = OT3Mount.LEFT if cfg.pipette_mount == "left" else OT3Mount.RIGHT - resources.ctx._core.get_hardware().retract(mnt) if ( not resources.ctx.is_simulating() and not cfg.same_tip and cfg.pipette_channels == 96 ): + + resources.pipette._retract() ui.get_user_ready("REPLACE first tip with NEW TIP") required_ul_per_src = (volume_for_setup * channel_count * cfg.trials) / len( cfg.dye_well_column_offset @@ -411,10 +407,8 @@ def _find_liquid_height( raise RuntimeError( f"bad volume in reservoir: {round(reservoir_ul / 1000, 1)} ml" ) - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) if not cfg.same_tip: resources.pipette.drop_tip(home_after=False) # always trash setup tips - resources.ctx._core.get_hardware().retract(OT3Mount.LEFT) # NOTE: the first tip-rack should have already been replaced # with new tips by the operator diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index 179701e0d839..7844f8d8d5ed 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -2,7 +2,7 @@ import asyncio from random import random, randint from types import MethodType -from typing import Any, List, Dict, Optional, Tuple +from typing import Any, List, Dict, Optional, Tuple, Union from statistics import stdev from . import config from .liquid_class.defaults import get_liquid_class @@ -15,21 +15,34 @@ guess_from_global_config as guess_deck_type_from_global_config, ) from opentrons.protocol_api.labware import Well, Labware +from opentrons.protocol_api._types import OffDeckType +from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocols.types import APIVersion from opentrons.hardware_control.thread_manager import ThreadManager from opentrons.hardware_control.types import OT3Mount, Axis from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control.instruments.ot3.pipette import Pipette +from opentrons import execute, simulate from opentrons.types import Point, Location from opentrons_shared_data.labware.dev_types import LabwareDefinition from hardware_testing.opentrons_api import helpers_ot3 from opentrons.protocol_api import ProtocolContext, InstrumentContext -from .workarounds import get_sync_hw_api, get_latest_offset_for_labware +from .workarounds import get_sync_hw_api from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm +import opentrons.protocol_engine.execution.pipetting as PE_pipetting +from opentrons.protocol_engine.notes import CommandNoteAdder + +from opentrons.protocol_engine import ( + StateView, + WellLocation, + DropTipWellLocation, +) +from opentrons.protocol_api.core.engine import deck_conflict as DeckConflit + def _add_fake_simulate( ctx: protocol_api.ProtocolContext, is_simulating: bool @@ -79,13 +92,21 @@ async def _thread_manager_build_hw_api( stall_detection_enable=stall_detection_enable, ) - return protocol_api.create_protocol_context( - api_version=APIVersion.from_string(api_level), - hardware_api=ThreadManager(_thread_manager_build_hw_api), # type: ignore[arg-type] - deck_type="ot3_standard", - extra_labware=extra_labware, - deck_version=2, - ) + papi: protocol_api.ProtocolContext + if is_simulating: + papi = simulate.get_protocol_api( + version=APIVersion.from_string(api_level), + extra_labware=extra_labware, + hardware_simulator=ThreadManager(_thread_manager_build_hw_api), + robot_type="Flex", + use_virtual_hardware=False, + ) + else: + papi = execute.get_protocol_api( + version=APIVersion.from_string(api_level), extra_labware=extra_labware + ) + + return papi def well_is_reservoir(well: protocol_api.labware.Well) -> bool: @@ -203,6 +224,50 @@ def _check_if_software_supports_high_volumes() -> bool: return modified_a and modified_b +def _override_set_current_volume(self, new_volume: float) -> None: # noqa: ANN001 + assert new_volume >= 0 + # assert new_volume <= self.working_volume + self._current_volume = new_volume + + +def _override_add_current_volume(self, volume_incr: float) -> None: # noqa: ANN001 + self._current_volume += volume_incr + + +def _override_ok_to_add_volume(self, volume_incr: float) -> bool: # noqa: ANN001 + return True + + +def _override_validate_asp_vol( + state_view: StateView, + pipette_id: str, + aspirate_volume: float, + command_note_adder: CommandNoteAdder, +) -> float: + return aspirate_volume + + +def _override_check_safe_for_pipette_movement( + engine_state: StateView, + pipette_id: str, + labware_id: str, + well_name: str, + well_location: Union[WellLocation, DropTipWellLocation], +) -> None: + pass + + +def _override_software_supports_high_volumes() -> None: + # yea so ok this is pretty ugly but this is super helpful for us + # with this we don't need to apply patches, and can run the testing scripts + # without pushing modified code to the robot + + Pipette.set_current_volume = _override_set_current_volume # type: ignore[assignment] + Pipette.ok_to_add_volume = _override_ok_to_add_volume # type: ignore[assignment] + Pipette.add_current_volume = _override_add_current_volume # type: ignore[assignment] + PE_pipetting._validate_aspirate_volume = _override_validate_asp_vol # type: ignore[assignment] + + def _get_channel_offset(cfg: config.VolumetricConfig, channel: int) -> Point: assert ( channel < cfg.pipette_channels @@ -252,23 +317,6 @@ def _get_tip_batch(is_simulating: bool, tip: int) -> str: return "simulation-tip-batch" -def _apply(labware: Labware, cfg: config.VolumetricConfig) -> None: - o = get_latest_offset_for_labware(cfg.labware_offsets, labware) - ui.print_info( - f'Apply labware offset to "{labware.name}" (slot={labware.parent}): ' - f"x={round(o.x, 2)}, y={round(o.y, 2)}, z={round(o.z, 2)}" - ) - labware.set_calibration(o) - - -def _apply_labware_offsets( - cfg: config.VolumetricConfig, - labwares: List[Labware], -) -> None: - for lw in labwares: - _apply(lw, cfg) - - def _pick_up_tip( ctx: ProtocolContext, pipette: InstrumentContext, @@ -280,8 +328,6 @@ def _pick_up_tip( f"from slot #{location.labware.parent.parent}" ) pipette.pick_up_tip(location) - if pipette.channels == 96: - get_sync_hw_api(ctx).retract(OT3Mount.LEFT) # NOTE: the accuracy-adjust function gets set on the Pipette # each time we pick-up a new tip. if cfg.increment: @@ -293,12 +339,27 @@ def _pick_up_tip( def _drop_tip( - pipette: InstrumentContext, return_tip: bool, minimum_z_height: int = 0 + pipette: InstrumentContext, + return_tip: bool, + minimum_z_height: int = 0, + offset: Optional[Point] = None, ) -> None: if return_tip: pipette.return_tip(home_after=False) else: - pipette.drop_tip(home_after=False) + if offset is not None: + # we don't actually need the offset, if this is an 8 channel we always center channel + # a1 over the back of the trash + trash_well = pipette.trash_container.well(0) # type: ignore[union-attr] + trash_container = trash_well.center().move( + Point(0, trash_well.width / 2, 0) # type: ignore[union-attr, operator] + ) + pipette.drop_tip( + trash_container, + home_after=False, + ) + else: + pipette.drop_tip(home_after=False) if minimum_z_height > 0: cur_location = pipette._get_last_location_by_api_version() if cur_location is not None: @@ -337,11 +398,8 @@ def _get_volumes( kind, channels, pipette_volume, tip_volume, extra ) if not _check_if_software_supports_high_volumes(): - if ctx.is_simulating(): - test_volumes = _reduce_volumes_to_not_exceed_software_limit( - test_volumes, pipette_volume, channels, tip_volume - ) - else: + _override_software_supports_high_volumes() + if not _check_if_software_supports_high_volumes(): raise RuntimeError("you are not the correct branch") return test_volumes @@ -363,7 +421,9 @@ def _load_pipette( if pipette_mount in loaded_pipettes.keys(): return loaded_pipettes[pipette_mount] + trash = ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") pipette = ctx.load_instrument(pip_name, pipette_mount) + loaded_pipettes = ctx.loaded_instruments assert pipette.max_volume == pipette_volume, ( f"expected {pipette_volume} uL pipette, " f"but got a {pipette.max_volume} uL pipette" @@ -374,12 +434,12 @@ def _load_pipette( # NOTE: 8ch QC testing means testing 1 channel at a time, # so we need to decrease the pick-up current to work with 1 tip. if pipette.channels == 8 and not increment and not photometric: - hwapi = get_sync_hw_api(ctx) - mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT - hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] - hwpipette._config.pick_up_tip_configurations.press_fit.current_by_tip_count[ - 8 - ] = 0.2 + pipette.configure_nozzle_layout(NozzleLayout.SINGLE, "A1") + # override deck conflict checking cause we specially lay out our tipracks + DeckConflit.check_safe_for_pipette_movement = ( + _override_check_safe_for_pipette_movement + ) + pipette.trash_container = trash return pipette @@ -402,23 +462,22 @@ def _load_tipracks( cfg: config.VolumetricConfig, use_adapters: bool = False, ) -> List[Labware]: - adp_str = "_adp" if use_adapters else "" tiprack_load_settings: List[Tuple[int, str]] = [ ( slot, - f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul{adp_str}", + f"opentrons_flex_96_tiprack_{cfg.tip_volume}ul", ) for slot in cfg.slots_tiprack ] for ls in tiprack_load_settings: ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') - if use_adapters: - tiprack_namespace = "custom_beta" - else: - tiprack_namespace = "opentrons" + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) # If running multiple tests in one run, the labware may already be loaded loaded_labwares = ctx.loaded_labwares + print(f"Loaded labwares {loaded_labwares}") pre_loaded_tips: List[Labware] = [] for ls in tiprack_load_settings: if ls[0] in loaded_labwares.keys(): @@ -430,15 +489,25 @@ def _load_tipracks( ui.print_info( f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" ) - del ctx._core.get_deck()[ls[0]] # type: ignore[attr-defined] + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) if len(pre_loaded_tips) == len(tiprack_load_settings): return pre_loaded_tips - tipracks = [ - ctx.load_labware(ls[1], location=ls[0], namespace=tiprack_namespace) - for ls in tiprack_load_settings - ] - _apply_labware_offsets(cfg, tipracks) + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) return tipracks diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index 1146d6bb4324..a37f21b1b361 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -11,8 +11,6 @@ _default_submerge_aspirate_mm = 1.5 _p50_multi_submerge_aspirate_mm = 1.5 _default_submerge_dispense_mm = 1.5 -_96_default_submerge_aspirate_mm = 2.5 -_96_default_submerge_dispense_mm = 3.0 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -273,7 +271,7 @@ 1000: { # P1000 50: { # T50 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -282,7 +280,7 @@ blow_out_submerged=5, ), 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -291,7 +289,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -302,7 +300,7 @@ }, 200: { # T200 5: DispenseSettings( # 5uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -311,7 +309,7 @@ blow_out_submerged=5, ), 50: DispenseSettings( # 50uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -320,7 +318,7 @@ blow_out_submerged=5, ), 200: DispenseSettings( # 200uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -331,7 +329,7 @@ }, 1000: { # T1000 10: DispenseSettings( # 10uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -340,7 +338,7 @@ blow_out_submerged=20, ), 100: DispenseSettings( # 100uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -349,7 +347,7 @@ blow_out_submerged=20, ), 1000: DispenseSettings( # 1000uL - z_submerge_depth=_96_default_submerge_dispense_mm, + z_submerge_depth=_default_submerge_dispense_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_dispense_delay_seconds, @@ -635,7 +633,7 @@ 1000: { # P1000 50: { # T50 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -645,7 +643,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -655,7 +653,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=6.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -667,7 +665,7 @@ }, 200: { # T200 5: AspirateSettings( # 5uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -677,7 +675,7 @@ trailing_air_gap=2, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -687,7 +685,7 @@ trailing_air_gap=3.5, ), 200: AspirateSettings( # 200uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=80, # ul/sec delay=_default_aspirate_delay_seconds, @@ -699,7 +697,7 @@ }, 1000: { # T1000 10: AspirateSettings( # 10uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -709,7 +707,7 @@ trailing_air_gap=10, ), 100: AspirateSettings( # 100uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, @@ -719,7 +717,7 @@ trailing_air_gap=10, ), 1000: AspirateSettings( # 1000uL - z_submerge_depth=_96_default_submerge_aspirate_mm, + z_submerge_depth=_default_submerge_aspirate_mm, plunger_acceleration=_default_accel_96ch_ul_sec_sec, plunger_flow_rate=160, # ul/sec delay=_default_aspirate_delay_seconds, diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py index 473877208eac..9f059559f133 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/pipetting.py @@ -8,6 +8,7 @@ from hardware_testing.opentrons_api.types import OT3AxisKind from hardware_testing.gravimetric import config +from hardware_testing.gravimetric.workarounds import get_sync_hw_api from hardware_testing.gravimetric.liquid_height.height import LiquidTracker from hardware_testing.opentrons_api.types import OT3Mount, Point from hardware_testing.opentrons_api.helpers_ot3 import clear_pipette_ul_per_mm @@ -177,7 +178,7 @@ def _pipette_with_liquid_settings( # noqa: C901 ) -> None: """Run a pipette given some Pipetting Liquid Settings.""" # FIXME: stop using hwapi, and get those functions into core software - hw_api = ctx._core.get_hardware() + hw_api = get_sync_hw_api(ctx) hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT hw_pipette = hw_api.hardware_pipettes[hw_mount.to_mount()] _check_aspirate_dispense_args(mix, aspirate, dispense) @@ -189,20 +190,6 @@ def _get_max_blow_out_ul() -> float: blow_out = hw_pipette.plunger_positions.blow_out return (blow_out - bottom) * blow_out_ul_per_mm - def _dispense_with_added_blow_out() -> None: - # dispense all liquid, plus some air - # FIXME: push-out is not supported in Legacy core, so here - # we again use the hardware controller - hw_api = ctx._core.get_hardware() - hw_mount = OT3Mount.LEFT if pipette.mount == "left" else OT3Mount.RIGHT - push_out = min(liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul()) - hw_api.dispense(hw_mount, push_out=push_out) - - def _blow_out_remaining_air() -> None: - # FIXME: using the HW-API to specify that we want to blow-out the full - # available blow-out volume - hw_api.blow_out(hw_mount, _get_max_blow_out_ul()) - # ASPIRATE/DISPENSE SEQUENCE HAS THREE PHASES: # 1. APPROACH # 2. SUBMERGE @@ -237,16 +224,17 @@ def _aspirate_on_approach() -> None: "WARNING: removing trailing air-gap from pipette, " "this should only happen during blank trials" ) - hw_api.dispense(hw_mount) + pipette.dispense(volume=pipette.current_volume) if mode: # NOTE: increment test requires the plunger's "bottom" position # does not change during the entire test run hw_api.set_liquid_class(hw_mount, mode) else: - hw_api.configure_for_volume(hw_mount, aspirate if aspirate else dispense) + cfg_volume: float = aspirate if aspirate else dispense # type: ignore[assignment] + pipette.configure_for_volume(cfg_volume) if clear_accuracy_function: clear_pipette_ul_per_mm(hw_api, hw_mount) # type: ignore[arg-type] - hw_api.prepare_for_aspirate(hw_mount) + pipette.prepare_to_aspirate() if liquid_class.aspirate.leading_air_gap > 0: pipette.aspirate(liquid_class.aspirate.leading_air_gap) @@ -260,14 +248,18 @@ def _aspirate_on_mix() -> None: if i < _num_mixes - 1: pipette.dispense(mix) else: - _dispense_with_added_blow_out() + if added_blow_out: + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) ctx.delay(liquid_class.dispense.delay) # don't go all the way up to retract position, but instead just above liquid _retract( ctx, pipette, well, channel_offset, approach_mm, retract_speed, _z_disc ) - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() assert pipette.current_volume == 0 def _aspirate_on_submerge() -> None: @@ -283,18 +275,22 @@ def _aspirate_on_submerge() -> None: def _aspirate_on_retract() -> None: # add trailing-air-gap - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) def _dispense_on_approach() -> None: # remove trailing-air-gap - pipette.dispense(liquid_class.aspirate.trailing_air_gap) + if not blank: + pipette.dispense(liquid_class.aspirate.trailing_air_gap) def _dispense_on_submerge() -> None: callbacks.on_dispensing() + push_out = None if added_blow_out: - _dispense_with_added_blow_out() - else: - pipette.dispense(dispense) + push_out = min( + liquid_class.dispense.blow_out_submerged, _get_max_blow_out_ul() + ) + pipette.dispense(dispense, push_out=push_out) # update liquid-height tracker liquid_tracker.update_affected_wells( well, dispense=dispense, channels=channel_count @@ -306,13 +302,13 @@ def _dispense_on_retract() -> None: if pipette.current_volume <= 0 and added_blow_out: # blow-out any remaining air in pipette (any reason why not?) callbacks.on_blowing_out() - _blow_out_remaining_air() - hw_api.prepare_for_aspirate(hw_mount) + pipette.blow_out() + pipette.prepare_to_aspirate() if touch_tip: pipette.touch_tip(speed=config.TOUCH_TIP_SPEED) # NOTE: always do a trailing-air-gap, regardless of if tip is empty or not # to avoid droplets from forming and falling off the tip - pipette.aspirate(liquid_class.aspirate.trailing_air_gap) + pipette.air_gap(liquid_class.aspirate.trailing_air_gap, height=0) # PHASE 1: APPROACH pipette.flow_rate.aspirate = liquid_class.aspirate.plunger_flow_rate @@ -337,7 +333,7 @@ def _dispense_on_retract() -> None: # EXIT callbacks.on_exiting() - hw_api.retract(hw_mount) + pipette._retract() def mix_with_liquid_class( diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/record.py b/hardware-testing/hardware_testing/gravimetric/measurement/record.py index d1e4ab7e4d4e..86ef8b84903a 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/record.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/record.py @@ -280,7 +280,11 @@ class GravimetricRecorder: """Gravimetric Recorder.""" def __init__( - self, cfg: GravimetricRecorderConfig, scale: Scale, simulate: bool = False + self, + cfg: GravimetricRecorderConfig, + scale: Scale, + simulate: bool = False, + start_graph: bool = True, ) -> None: """Gravimetric Recorder.""" self._cfg = cfg @@ -294,7 +298,7 @@ def __init__( self._scale_serial: str = "" self._scale_max_capacity: float = 0.0 super().__init__() - self.activate() + self.activate(start_graph) def _start_graph_server_process(self) -> None: if self.is_simulator: @@ -350,9 +354,10 @@ def add_simulation_mass(self, mass: float) -> None: """Add simulation mass.""" self._scale.add_simulation_mass(mass) - def activate(self) -> None: + def activate(self, graph: bool = True) -> None: """Activate.""" - self._start_graph_server_process() + if graph: + self._start_graph_server_process() # Some Radwag settings cannot be controlled remotely. # Listed below are the things the must be done using the touchscreen: # 1) Set profile to USER diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch index 4e2ab9b6c236..e69de29bb2d1 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch @@ -1,111 +0,0 @@ -diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -index 2d36460ca6..8578768930 100644 ---- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -+++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -@@ -427,11 +427,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - - def set_current_volume(self, new_volume: float) -> None: - assert new_volume >= 0 -- assert new_volume <= self.working_volume -+ # assert new_volume <= self.working_volume - self._current_volume = new_volume - - def add_current_volume(self, volume_incr: float) -> None: -- assert self.ok_to_add_volume(volume_incr) -+ # assert self.ok_to_add_volume(volume_incr) - self._current_volume += volume_incr - - def remove_current_volume(self, volume_incr: float) -> None: -@@ -439,7 +439,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): - self._current_volume -= volume_incr - - def ok_to_add_volume(self, volume_incr: float) -> bool: -- return self.current_volume + volume_incr <= self.working_volume -+ # return self.current_volume + volume_incr <= self.working_volume -+ return True - - def ok_to_push_out(self, push_out_dist_mm: float) -> bool: - return push_out_dist_mm <= ( -diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -index 0ba7e17621..4d6682f5e4 100644 ---- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -+++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py -@@ -341,18 +341,12 @@ def check_safe_for_tip_pickup_and_return( - f" when picking up fewer than 96 tips." - ) - elif not is_partial_config and not is_96_ch_tiprack_adapter: -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - elif ( - not is_partial_config - ): # tiprack is not on adapter and pipette is in full config -- raise UnsuitableTiprackForPipetteMotion( -- f"{tiprack_name} must be on an Opentrons Flex 96 Tip Rack Adapter" -- f" in order to pick up or return all 96 tips simultaneously." -- ) -+ pass - - - # TODO (spp, 2023-02-06): update the extents check to use all nozzle bounds instead of -diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py -index 9a9092af5a..33aa5941ce 100644 ---- a/api/src/opentrons/protocol_api/core/legacy/deck.py -+++ b/api/src/opentrons/protocol_api/core/legacy/deck.py -@@ -55,11 +55,11 @@ class DeckItem(Protocol): - class Deck(UserDict): # type: ignore[type-arg] - data: Dict[int, Optional[DeckItem]] - -- def __init__(self, deck_type: str) -> None: -+ def __init__( -+ self, deck_type: str, version: int = DEFAULT_LEGACY_DECK_DEFINITION_VERSION -+ ) -> None: - super().__init__() -- self._definition = load_deck( -- name=deck_type, version=DEFAULT_LEGACY_DECK_DEFINITION_VERSION -- ) -+ self._definition = load_deck(name=deck_type, version=version) - self._positions = {} - for slot in self._definition["locations"]["orderedSlots"]: - self.data[int(slot["id"])] = None -diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py -index 5a64e70cf9..7d5047cc4b 100644 ---- a/api/src/opentrons/protocol_api/create_protocol_context.py -+++ b/api/src/opentrons/protocol_api/create_protocol_context.py -@@ -22,6 +22,7 @@ from .deck import Deck - - from .core.common import ProtocolCore as AbstractProtocolCore - from .core.legacy.deck import Deck as LegacyDeck -+from opentrons_shared_data.deck import DEFAULT_DECK_DEFINITION_VERSION - from .core.legacy.legacy_protocol_core import LegacyProtocolCore - from .core.legacy.labware_offset_provider import ( - AbstractLabwareOffsetProvider, -@@ -52,6 +53,7 @@ def create_protocol_context( - extra_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_labware: Optional[Dict[str, LabwareDefinition]] = None, - bundled_data: Optional[Dict[str, bytes]] = None, -+ deck_version: int = DEFAULT_DECK_DEFINITION_VERSION, - ) -> ProtocolContext: - """Create a ProtocolContext for use in a Python protocol. - -@@ -121,7 +123,7 @@ def create_protocol_context( - - # TODO(mc, 2022-8-22): remove `disable_fast_protocol_upload` - elif use_simulating_core and not feature_flags.disable_fast_protocol_upload(): -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCoreSimulator( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, -@@ -133,7 +135,7 @@ def create_protocol_context( - ) - - else: -- legacy_deck = LegacyDeck(deck_type=deck_type) -+ legacy_deck = LegacyDeck(deck_type=deck_type, version=deck_version) - core = LegacyProtocolCore( - sync_hardware=sync_hardware, - labware_offset_provider=labware_offset_provider, diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch index b2d08d109e9b..5d688841b918 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch @@ -1,872 +1,180 @@ -diff --git a/shared-data/deck/definitions/2/ot3_standard.json b/shared-data/deck/definitions/2/ot3_standard.json -new file mode 100644 -index 0000000000..8ad4397cba ---- /dev/null -+++ b/shared-data/deck/definitions/2/ot3_standard.json -@@ -0,0 +1,866 @@ -+{ -+ "otId": "ot3_standard", -+ "schemaVersion": 3, -+ "cornerOffsetFromOrigin": [-204.31, -76.59, 0], -+ "dimensions": [854.995, 581.74, 0], -+ "metadata": { -+ "displayName": "OT-3 Standard Deck", -+ "tags": ["ot3", "12 slots", "standard"] -+ }, -+ "robot": { -+ "model": "OT-3 Standard" -+ }, -+ "locations": { -+ "orderedSlots": [ -+ { -+ "id": "1", -+ "position": [0.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "2", -+ "position": [164.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "3", -+ "position": [328.0, 0.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot D3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "4", -+ "position": [0.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "5", -+ "position": [164.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "6", -+ "position": [328.0, 107, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot C3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "7", -+ "position": [0.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "thermocyclerModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "8", -+ "position": [164.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "9", -+ "position": [328.0, 214.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot B3", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "10", -+ "position": [0.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A1", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "11", -+ "position": [164.0, 321.0, 0.0], -+ "matingSurfaceUnitVector": [-1, 1, -1], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A2", -+ "compatibleModuleTypes": [ -+ "magneticModuleType", -+ "temperatureModuleType", -+ "heaterShakerModuleType" -+ ] -+ }, -+ { -+ "id": "12", -+ "position": [328.0, 321.0, 0.0], -+ "boundingBox": { -+ "xDimension": 128.0, -+ "yDimension": 86.0, -+ "zDimension": 0 -+ }, -+ "displayName": "Slot A3", -+ "compatibleModuleTypes": [] -+ } -+ ], -+ "calibrationPoints": [], -+ "fixtures": [ -+ { -+ "id": "fixedTrash", -+ "slot": "12", -+ "labware": "opentrons_1_trash_3200ml_fixed", -+ "displayName": "Fixed Trash" -+ } -+ ] -+ }, -+ "layers": [ -+ { -+ "name": "style", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "type": "text/css" -+ }, -+ "children": [ -+ { -+ "name": "", -+ "type": "text", -+ "value": "\n.st0{fill:#CCCCCC;}\n.st1{fill:none;stroke:#16212D;stroke-width:3.2047;stroke-opacity:0.7;}\n.st2{fill:none;stroke:#16212D;stroke-width:3.156;stroke-opacity:0.7;}\n", -+ "attributes": {}, -+ "children": [] -+ } -+ ] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_A1_EXPANSION", -+ "class": "st0", -+ "d": "M-97.8,496.6h239c2.3,0,4.2-1.9,4.2-4.2v-70c0-2.3-1.9-4.2-4.2-4.2h-239c-2.3,0-4.2,1.9-4.2,4.2v70\nC-102,494.7-100.1,496.6-97.8,496.6z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A1", -+ "class": "st0", -+ "d": "M-97.7,417.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,415.1-100.1,417.1-97.7,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A2", -+ "class": "st0", -+ "d": "M150.8,417.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,415.1,148.4,417.1,150.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_A3", -+ "class": "st0", -+ "d": "M314.8,417.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,415.1,312.4,417.1,314.8,417.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B1", -+ "class": "st0", -+ "d": "M-97.7,310h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC-102,308.1-100.1,310-97.7,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B2", -+ "class": "st0", -+ "d": "M150.8,310h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC146.5,308.1,148.4,310,150.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_B3", -+ "class": "st0", -+ "d": "M314.8,310h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.2c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.2\nC310.5,308.1,312.4,310,314.8,310z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C1", -+ "class": "st0", -+ "d": "M-97.7,203.1h238.8c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,201.2-100.1,203.1-97.7,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C2", -+ "class": "st0", -+ "d": "M150.8,203.1h154.3c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,201.2,148.4,203.1,150.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_C3", -+ "class": "st0", -+ "d": "M314.8,203.1h238.9c2.4,0,4.3-1.9,4.3-4.3v-97.4c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,201.2,312.4,203.1,314.8,203.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D1", -+ "class": "st0", -+ "d": "M-97.7,96.1h238.8c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H-97.7c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC-102,94.2-100.1,96.1-97.7,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D2", -+ "class": "st0", -+ "d": "M150.8,96.1h154.3c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H150.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC146.5,94.2,148.4,96.1,150.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_BASE_D3", -+ "class": "st0", -+ "d": "M314.8,96.1h238.9c2.4,0,4.3-1.9,4.3-4.3V-5.6c0-2.4-1.9-4.3-4.3-4.3H314.8c-2.4,0-4.3,1.9-4.3,4.3v97.4\nC310.5,94.2,312.4,96.1,314.8,96.1z" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "g", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "id": "SLOT_CLIPS" -+ }, -+ "children": [ -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,398.9V409H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,329.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,398.9V409h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,329.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,398.9V409h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,329.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,398.9V409H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,329.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,291.9V302H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,222.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,291.9V302h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,222.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,291.9V302h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,222.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,291.9V302H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,222.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,185v10.1H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,115.8v-10.5H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,185v10.1h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,115.8v-10.7h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,185v10.1h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,115.8v-10.5h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,185v10.1H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,115.8v-10.7H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M-1.9,77.9V88H8.9" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M-1.9,8.8V-1.7H8.7" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M129.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M162.1,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M162.1,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,77.9V88h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M293.9,8.8V-1.9h-10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M326,77.9V88h10.8" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st2", -+ "d": "M326,8.8V-1.7h10.6" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,77.9V88H447" -+ }, -+ "children": [] -+ }, -+ { -+ "name": "path", -+ "type": "element", -+ "value": "", -+ "attributes": { -+ "class": "st1", -+ "d": "M457.8,8.8V-1.9H447" -+ }, -+ "children": [] -+ } -+ ] -+ } -+ ] -+} +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +index c798ce421a..14fc4a5b67 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +@@ -20,50 +20,50 @@ + "aspirate": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.31, 0.591, 0.0197], ++ [0.39, 0.2586, 0.1227], ++ [0.86, 0.3697, 0.0794], ++ [1.29, 0.231, 0.1987], ++ [1.93, 0.1144, 0.3491], ++ [2.7, 0.0536, 0.4664], ++ [2.95, -0.1041, 0.8923], ++ [3.28, 0.0216, 0.5214], ++ [3.76, 0.048, 0.4349], ++ [4.38, 0.083, 0.3032], ++ [5.08, 0.0153, 0.5996], ++ [5.9, 0.0136, 0.6083], ++ [7.29, 0.007, 0.6474], ++ [9.04, 0.0059, 0.6551], ++ [10.08, 0.0045, 0.6682], ++ [13.74, 0.0029, 0.6842], ++ [27.15, 0.001, 0.7104], ++ [50.48, 0.0002, 0.7319], ++ [52.89, -0.0006, 0.7703] + ] + } + }, +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +index 644d93354e..4eba92a089 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +@@ -20,46 +20,48 @@ + "aspirate": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3, 0.459, 0.0586], ++ [0.47, 0.43, 0.0674], ++ [0.9, 0.3404, 0.1095], ++ [1.26, 0.1925, 0.2425], ++ [1.95, 0.1314, 0.3195], ++ [2.76, 0.0604, 0.458], ++ [2.95, -0.2085, 1.2002], ++ [3.33, 0.0425, 0.4597], ++ [3.87, 0.0592, 0.404], ++ [4.31, 0.0518, 0.4327], ++ [5.07, 0.0264, 0.5424], ++ [5.93, 0.0186, 0.5818], ++ [7.34, 0.0078, 0.6458], ++ [9.08, 0.005, 0.6664], ++ [10.09, 0.0022, 0.6918], ++ [13.74, 0.0027, 0.6868], ++ [27.13, 0.0009, 0.7109], ++ [45.43, -0.0038, 0.8391] + ] + } + }, diff --git a/hardware-testing/hardware_testing/gravimetric/tips.py b/hardware-testing/hardware_testing/gravimetric/tips.py index 8edf66a57975..7e72c6884a24 100644 --- a/hardware-testing/hardware_testing/gravimetric/tips.py +++ b/hardware-testing/hardware_testing/gravimetric/tips.py @@ -60,18 +60,18 @@ 7: "A", } CHANNEL_TO_TIP_ROW_LOOKUP_BY_SLOT = { - "1": CHANNEL_TO_TIP_ROW_LOOKUP, - "2": CHANNEL_TO_TIP_ROW_LOOKUP, - "3": CHANNEL_TO_TIP_ROW_LOOKUP, - "4": CHANNEL_TO_TIP_ROW_LOOKUP, - "5": CHANNEL_TO_TIP_ROW_LOOKUP, - "6": CHANNEL_TO_TIP_ROW_LOOKUP, - "7": CHANNEL_TO_TIP_ROW_LOOKUP, - "8": CHANNEL_TO_TIP_ROW_LOOKUP, - "9": CHANNEL_TO_TIP_ROW_LOOKUP, - "10": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "11": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, - "12": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "D1": CHANNEL_TO_TIP_ROW_LOOKUP, + "D2": CHANNEL_TO_TIP_ROW_LOOKUP, + "D3": CHANNEL_TO_TIP_ROW_LOOKUP, + "C1": CHANNEL_TO_TIP_ROW_LOOKUP, + "C2": CHANNEL_TO_TIP_ROW_LOOKUP, + "C3": CHANNEL_TO_TIP_ROW_LOOKUP, + "B1": CHANNEL_TO_TIP_ROW_LOOKUP, + "B2": CHANNEL_TO_TIP_ROW_LOOKUP, + "B3": CHANNEL_TO_TIP_ROW_LOOKUP, + "A1": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A2": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, + "A3": CHANNEL_TO_TIP_ROW_LOOKUP_BACK, } REAR_CHANNELS = [0, 1, 2, 3] FRONT_CHANNELS = [4, 5, 6, 7] diff --git a/hardware-testing/hardware_testing/gravimetric/workarounds.py b/hardware-testing/hardware_testing/gravimetric/workarounds.py index 0d2c425d8307..7c182ddd0794 100644 --- a/hardware-testing/hardware_testing/gravimetric/workarounds.py +++ b/hardware-testing/hardware_testing/gravimetric/workarounds.py @@ -12,6 +12,8 @@ from hardware_testing.opentrons_api.helpers_ot3 import start_server_ot3, stop_server_ot3 from hardware_testing.opentrons_api.types import Point +from opentrons.protocol_engine.types import LabwareOffset + def is_running_in_app() -> bool: """Is running in App.""" @@ -33,7 +35,7 @@ def force_prepare_for_aspirate(pipette: InstrumentContext) -> None: pipette.dispense() -def http_get_all_labware_offsets() -> List[dict]: +def http_get_all_labware_offsets() -> List[LabwareOffset]: """Request (HTTP GET) from the local robot-server all runs information.""" req = Request("http://localhost:31950/runs") req.add_header("Opentrons-Version", "2") @@ -46,7 +48,18 @@ def http_get_all_labware_offsets() -> List[dict]: runs_json = json_loads(runs_response_data) protocols_list = runs_json["data"] - return [offset for p in protocols_list for offset in p["labwareOffsets"]] + offset_dict = [offset for p in protocols_list for offset in p["labwareOffsets"]] + offsets: List[LabwareOffset] = [] + for offset_data in offset_dict: + new_offset = LabwareOffset( + id=offset_data["id"], + createdAt=offset_data["createdAt"], + definitionUri=offset_data["definitionUri"], + location=offset_data["location"], + vector=offset_data["vector"], + ) + offsets.append(new_offset) + return offsets def _old_slot_to_ot3_slot(old_api_slot: str) -> str: diff --git a/hardware-testing/hardware_testing/labware/dial_indicator/1.json b/hardware-testing/hardware_testing/labware/dial_indicator/1.json new file mode 100644 index 000000000000..6c3ac9c3f244 --- /dev/null +++ b/hardware-testing/hardware_testing/labware/dial_indicator/1.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 2, + "version": 1, + "namespace": "custom_beta", + "ordering": [["A1"]], + "metadata": { + "displayName": "Mitutoyo Digimatic Indicator", + "displayCategory": "tubeRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 128, + "yDimension": 86, + "zDimension": 136 + }, + "parameters": { + "format": "irregular", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "dial_indicator" + }, + "wells": { + "A1": { + "depth": 14, + "totalLiquidVolume": 10, + "shape": "circular", + "diameter": 4, + "x": 60.8, + "y": 41.5, + "z": 135 + } + }, + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "groups": [ + { + "brand": { + "brand": "Mitutoyo", + "brandId": ["ID-S"] + }, + "metadata": { + "wellBottomShape": "flat", + "displayCategory": "tubeRack" + }, + "wells": ["A1"] + } + ], + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + } +} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json deleted file mode 100644 index 2307f25d8760..000000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_1000ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 1000 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 74.1, - "z": 36.4 - }, - "B1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 65.1, - "z": 36.4 - }, - "C1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 56.1, - "z": 36.4 - }, - "D1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 47.1, - "z": 36.4 - }, - "E1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 38.1, - "z": 36.4 - }, - "F1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 29.1, - "z": 36.4 - }, - "G1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 20.1, - "z": 36.4 - }, - "H1": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 14.38, - "y": 11.1, - "z": 36.4 - }, - "A2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 74.1, - "z": 36.4 - }, - "B2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 65.1, - "z": 36.4 - }, - "C2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 56.1, - "z": 36.4 - }, - "D2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 47.1, - "z": 36.4 - }, - "E2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 38.1, - "z": 36.4 - }, - "F2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 29.1, - "z": 36.4 - }, - "G2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 20.1, - "z": 36.4 - }, - "H2": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 23.38, - "y": 11.1, - "z": 36.4 - }, - "A3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 74.1, - "z": 36.4 - }, - "B3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 65.1, - "z": 36.4 - }, - "C3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 56.1, - "z": 36.4 - }, - "D3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 47.1, - "z": 36.4 - }, - "E3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 38.1, - "z": 36.4 - }, - "F3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 29.1, - "z": 36.4 - }, - "G3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 20.1, - "z": 36.4 - }, - "H3": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 32.38, - "y": 11.1, - "z": 36.4 - }, - "A4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 74.1, - "z": 36.4 - }, - "B4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 65.1, - "z": 36.4 - }, - "C4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 56.1, - "z": 36.4 - }, - "D4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 47.1, - "z": 36.4 - }, - "E4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 38.1, - "z": 36.4 - }, - "F4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 29.1, - "z": 36.4 - }, - "G4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 20.1, - "z": 36.4 - }, - "H4": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 41.38, - "y": 11.1, - "z": 36.4 - }, - "A5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 74.1, - "z": 36.4 - }, - "B5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 65.1, - "z": 36.4 - }, - "C5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 56.1, - "z": 36.4 - }, - "D5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 47.1, - "z": 36.4 - }, - "E5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 38.1, - "z": 36.4 - }, - "F5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 29.1, - "z": 36.4 - }, - "G5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 20.1, - "z": 36.4 - }, - "H5": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 50.38, - "y": 11.1, - "z": 36.4 - }, - "A6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 74.1, - "z": 36.4 - }, - "B6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 65.1, - "z": 36.4 - }, - "C6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 56.1, - "z": 36.4 - }, - "D6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 47.1, - "z": 36.4 - }, - "E6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 38.1, - "z": 36.4 - }, - "F6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 29.1, - "z": 36.4 - }, - "G6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 20.1, - "z": 36.4 - }, - "H6": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 59.38, - "y": 11.1, - "z": 36.4 - }, - "A7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 74.1, - "z": 36.4 - }, - "B7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 65.1, - "z": 36.4 - }, - "C7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 56.1, - "z": 36.4 - }, - "D7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 47.1, - "z": 36.4 - }, - "E7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 38.1, - "z": 36.4 - }, - "F7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 29.1, - "z": 36.4 - }, - "G7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 20.1, - "z": 36.4 - }, - "H7": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 68.38, - "y": 11.1, - "z": 36.4 - }, - "A8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 74.1, - "z": 36.4 - }, - "B8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 65.1, - "z": 36.4 - }, - "C8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 56.1, - "z": 36.4 - }, - "D8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 47.1, - "z": 36.4 - }, - "E8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 38.1, - "z": 36.4 - }, - "F8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 29.1, - "z": 36.4 - }, - "G8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 20.1, - "z": 36.4 - }, - "H8": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 77.38, - "y": 11.1, - "z": 36.4 - }, - "A9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 74.1, - "z": 36.4 - }, - "B9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 65.1, - "z": 36.4 - }, - "C9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 56.1, - "z": 36.4 - }, - "D9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 47.1, - "z": 36.4 - }, - "E9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 38.1, - "z": 36.4 - }, - "F9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 29.1, - "z": 36.4 - }, - "G9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 20.1, - "z": 36.4 - }, - "H9": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 86.38, - "y": 11.1, - "z": 36.4 - }, - "A10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 74.1, - "z": 36.4 - }, - "B10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 65.1, - "z": 36.4 - }, - "C10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 56.1, - "z": 36.4 - }, - "D10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 47.1, - "z": 36.4 - }, - "E10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 38.1, - "z": 36.4 - }, - "F10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 29.1, - "z": 36.4 - }, - "G10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 20.1, - "z": 36.4 - }, - "H10": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 95.38, - "y": 11.1, - "z": 36.4 - }, - "A11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 74.1, - "z": 36.4 - }, - "B11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 65.1, - "z": 36.4 - }, - "C11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 56.1, - "z": 36.4 - }, - "D11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 47.1, - "z": 36.4 - }, - "E11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 38.1, - "z": 36.4 - }, - "F11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 29.1, - "z": 36.4 - }, - "G11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 20.1, - "z": 36.4 - }, - "H11": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 104.38, - "y": 11.1, - "z": 36.4 - }, - "A12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 74.1, - "z": 36.4 - }, - "B12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 65.1, - "z": 36.4 - }, - "C12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 56.1, - "z": 36.4 - }, - "D12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 47.1, - "z": 36.4 - }, - "E12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 38.1, - "z": 36.4 - }, - "F12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 29.1, - "z": 36.4 - }, - "G12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 20.1, - "z": 36.4 - }, - "H12": { - "depth": 95.6, - "totalLiquidVolume": 1000, - "shape": "circular", - "diameter": 5.47, - "x": 113.38, - "y": 11.1, - "z": 36.4 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 95.6, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_1000ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json deleted file mode 100644 index 439479d5c76e..000000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_200ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 200 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 74.1, - "z": 73.65 - }, - "B1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 65.1, - "z": 73.65 - }, - "C1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 56.1, - "z": 73.65 - }, - "D1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 47.1, - "z": 73.65 - }, - "E1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 38.1, - "z": 73.65 - }, - "F1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 29.1, - "z": 73.65 - }, - "G1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 20.1, - "z": 73.65 - }, - "H1": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 14.38, - "y": 11.1, - "z": 73.65 - }, - "A2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 74.1, - "z": 73.65 - }, - "B2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 65.1, - "z": 73.65 - }, - "C2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 56.1, - "z": 73.65 - }, - "D2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 47.1, - "z": 73.65 - }, - "E2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 38.1, - "z": 73.65 - }, - "F2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 29.1, - "z": 73.65 - }, - "G2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 20.1, - "z": 73.65 - }, - "H2": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 23.38, - "y": 11.1, - "z": 73.65 - }, - "A3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 74.1, - "z": 73.65 - }, - "B3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 65.1, - "z": 73.65 - }, - "C3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 56.1, - "z": 73.65 - }, - "D3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 47.1, - "z": 73.65 - }, - "E3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 38.1, - "z": 73.65 - }, - "F3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 29.1, - "z": 73.65 - }, - "G3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 20.1, - "z": 73.65 - }, - "H3": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 32.38, - "y": 11.1, - "z": 73.65 - }, - "A4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 74.1, - "z": 73.65 - }, - "B4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 65.1, - "z": 73.65 - }, - "C4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 56.1, - "z": 73.65 - }, - "D4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 47.1, - "z": 73.65 - }, - "E4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 38.1, - "z": 73.65 - }, - "F4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 29.1, - "z": 73.65 - }, - "G4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 20.1, - "z": 73.65 - }, - "H4": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 41.38, - "y": 11.1, - "z": 73.65 - }, - "A5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 74.1, - "z": 73.65 - }, - "B5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 65.1, - "z": 73.65 - }, - "C5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 56.1, - "z": 73.65 - }, - "D5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 47.1, - "z": 73.65 - }, - "E5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 38.1, - "z": 73.65 - }, - "F5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 29.1, - "z": 73.65 - }, - "G5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 20.1, - "z": 73.65 - }, - "H5": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 50.38, - "y": 11.1, - "z": 73.65 - }, - "A6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 74.1, - "z": 73.65 - }, - "B6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 65.1, - "z": 73.65 - }, - "C6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 56.1, - "z": 73.65 - }, - "D6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 47.1, - "z": 73.65 - }, - "E6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 38.1, - "z": 73.65 - }, - "F6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 29.1, - "z": 73.65 - }, - "G6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 20.1, - "z": 73.65 - }, - "H6": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 59.38, - "y": 11.1, - "z": 73.65 - }, - "A7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 74.1, - "z": 73.65 - }, - "B7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 65.1, - "z": 73.65 - }, - "C7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 56.1, - "z": 73.65 - }, - "D7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 47.1, - "z": 73.65 - }, - "E7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 38.1, - "z": 73.65 - }, - "F7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 29.1, - "z": 73.65 - }, - "G7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 20.1, - "z": 73.65 - }, - "H7": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 68.38, - "y": 11.1, - "z": 73.65 - }, - "A8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 74.1, - "z": 73.65 - }, - "B8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 65.1, - "z": 73.65 - }, - "C8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 56.1, - "z": 73.65 - }, - "D8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 47.1, - "z": 73.65 - }, - "E8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 38.1, - "z": 73.65 - }, - "F8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 29.1, - "z": 73.65 - }, - "G8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 20.1, - "z": 73.65 - }, - "H8": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 77.38, - "y": 11.1, - "z": 73.65 - }, - "A9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 74.1, - "z": 73.65 - }, - "B9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 65.1, - "z": 73.65 - }, - "C9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 56.1, - "z": 73.65 - }, - "D9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 47.1, - "z": 73.65 - }, - "E9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 38.1, - "z": 73.65 - }, - "F9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 29.1, - "z": 73.65 - }, - "G9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 20.1, - "z": 73.65 - }, - "H9": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 86.38, - "y": 11.1, - "z": 73.65 - }, - "A10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 74.1, - "z": 73.65 - }, - "B10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 65.1, - "z": 73.65 - }, - "C10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 56.1, - "z": 73.65 - }, - "D10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 47.1, - "z": 73.65 - }, - "E10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 38.1, - "z": 73.65 - }, - "F10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 29.1, - "z": 73.65 - }, - "G10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 20.1, - "z": 73.65 - }, - "H10": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 95.38, - "y": 11.1, - "z": 73.65 - }, - "A11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 74.1, - "z": 73.65 - }, - "B11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 65.1, - "z": 73.65 - }, - "C11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 56.1, - "z": 73.65 - }, - "D11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 47.1, - "z": 73.65 - }, - "E11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 38.1, - "z": 73.65 - }, - "F11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 29.1, - "z": 73.65 - }, - "G11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 20.1, - "z": 73.65 - }, - "H11": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 104.38, - "y": 11.1, - "z": 73.65 - }, - "A12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 74.1, - "z": 73.65 - }, - "B12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 65.1, - "z": 73.65 - }, - "C12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 56.1, - "z": 73.65 - }, - "D12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 47.1, - "z": 73.65 - }, - "E12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 38.1, - "z": 73.65 - }, - "F12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 29.1, - "z": 73.65 - }, - "G12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 20.1, - "z": 73.65 - }, - "H12": { - "depth": 58.35, - "totalLiquidVolume": 200, - "shape": "circular", - "diameter": 5.59, - "x": 113.38, - "y": 11.1, - "z": 73.65 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 58.35, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_200ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json b/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json deleted file mode 100644 index a4d1b3390976..000000000000 --- a/hardware-testing/hardware_testing/labware/opentrons_flex_96_tiprack_50ul_adp/1.json +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "ordering": [ - ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], - ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], - ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], - ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], - ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], - ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], - ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], - ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], - ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], - ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], - ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], - ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] - ], - "brand": { - "brand": "ryantrons OT-3", - "brandId": [] - }, - "metadata": { - "displayName": "Opentrons Flex 96 Tip Rack 50 µL with adapter", - "displayCategory": "tipRack", - "displayVolumeUnits": "µL", - "tags": [] - }, - "dimensions": { - "xDimension": 127.76, - "yDimension": 85.48, - "zDimension": 132 - }, - "wells": { - "A1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 74.1, - "z": 74.1 - }, - "B1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 65.1, - "z": 74.1 - }, - "C1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 56.1, - "z": 74.1 - }, - "D1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 47.1, - "z": 74.1 - }, - "E1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 38.1, - "z": 74.1 - }, - "F1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 29.1, - "z": 74.1 - }, - "G1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 20.1, - "z": 74.1 - }, - "H1": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 14.38, - "y": 11.1, - "z": 74.1 - }, - "A2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 74.1, - "z": 74.1 - }, - "B2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 65.1, - "z": 74.1 - }, - "C2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 56.1, - "z": 74.1 - }, - "D2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 47.1, - "z": 74.1 - }, - "E2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 38.1, - "z": 74.1 - }, - "F2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 29.1, - "z": 74.1 - }, - "G2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 20.1, - "z": 74.1 - }, - "H2": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 23.38, - "y": 11.1, - "z": 74.1 - }, - "A3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 74.1, - "z": 74.1 - }, - "B3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 65.1, - "z": 74.1 - }, - "C3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 56.1, - "z": 74.1 - }, - "D3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 47.1, - "z": 74.1 - }, - "E3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 38.1, - "z": 74.1 - }, - "F3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 29.1, - "z": 74.1 - }, - "G3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 20.1, - "z": 74.1 - }, - "H3": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 32.38, - "y": 11.1, - "z": 74.1 - }, - "A4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 74.1, - "z": 74.1 - }, - "B4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 65.1, - "z": 74.1 - }, - "C4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 56.1, - "z": 74.1 - }, - "D4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 47.1, - "z": 74.1 - }, - "E4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 38.1, - "z": 74.1 - }, - "F4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 29.1, - "z": 74.1 - }, - "G4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 20.1, - "z": 74.1 - }, - "H4": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 41.38, - "y": 11.1, - "z": 74.1 - }, - "A5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 74.1, - "z": 74.1 - }, - "B5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 65.1, - "z": 74.1 - }, - "C5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 56.1, - "z": 74.1 - }, - "D5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 47.1, - "z": 74.1 - }, - "E5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 38.1, - "z": 74.1 - }, - "F5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 29.1, - "z": 74.1 - }, - "G5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 20.1, - "z": 74.1 - }, - "H5": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 50.38, - "y": 11.1, - "z": 74.1 - }, - "A6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 74.1, - "z": 74.1 - }, - "B6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 65.1, - "z": 74.1 - }, - "C6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 56.1, - "z": 74.1 - }, - "D6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 47.1, - "z": 74.1 - }, - "E6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 38.1, - "z": 74.1 - }, - "F6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 29.1, - "z": 74.1 - }, - "G6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 20.1, - "z": 74.1 - }, - "H6": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 59.38, - "y": 11.1, - "z": 74.1 - }, - "A7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 74.1, - "z": 74.1 - }, - "B7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 65.1, - "z": 74.1 - }, - "C7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 56.1, - "z": 74.1 - }, - "D7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 47.1, - "z": 74.1 - }, - "E7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 38.1, - "z": 74.1 - }, - "F7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 29.1, - "z": 74.1 - }, - "G7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 20.1, - "z": 74.1 - }, - "H7": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 68.38, - "y": 11.1, - "z": 74.1 - }, - "A8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 74.1, - "z": 74.1 - }, - "B8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 65.1, - "z": 74.1 - }, - "C8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 56.1, - "z": 74.1 - }, - "D8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 47.1, - "z": 74.1 - }, - "E8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 38.1, - "z": 74.1 - }, - "F8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 29.1, - "z": 74.1 - }, - "G8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 20.1, - "z": 74.1 - }, - "H8": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 77.38, - "y": 11.1, - "z": 74.1 - }, - "A9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 74.1, - "z": 74.1 - }, - "B9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 65.1, - "z": 74.1 - }, - "C9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 56.1, - "z": 74.1 - }, - "D9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 47.1, - "z": 74.1 - }, - "E9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 38.1, - "z": 74.1 - }, - "F9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 29.1, - "z": 74.1 - }, - "G9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 20.1, - "z": 74.1 - }, - "H9": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 86.38, - "y": 11.1, - "z": 74.1 - }, - "A10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 74.1, - "z": 74.1 - }, - "B10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 65.1, - "z": 74.1 - }, - "C10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 56.1, - "z": 74.1 - }, - "D10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 47.1, - "z": 74.1 - }, - "E10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 38.1, - "z": 74.1 - }, - "F10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 29.1, - "z": 74.1 - }, - "G10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 20.1, - "z": 74.1 - }, - "H10": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 95.38, - "y": 11.1, - "z": 74.1 - }, - "A11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 74.1, - "z": 74.1 - }, - "B11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 65.1, - "z": 74.1 - }, - "C11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 56.1, - "z": 74.1 - }, - "D11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 47.1, - "z": 74.1 - }, - "E11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 38.1, - "z": 74.1 - }, - "F11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 29.1, - "z": 74.1 - }, - "G11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 20.1, - "z": 74.1 - }, - "H11": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 104.38, - "y": 11.1, - "z": 74.1 - }, - "A12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 74.1, - "z": 74.1 - }, - "B12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 65.1, - "z": 74.1 - }, - "C12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 56.1, - "z": 74.1 - }, - "D12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 47.1, - "z": 74.1 - }, - "E12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 38.1, - "z": 74.1 - }, - "F12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 29.1, - "z": 74.1 - }, - "G12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 20.1, - "z": 74.1 - }, - "H12": { - "depth": 57.9, - "totalLiquidVolume": 50, - "shape": "circular", - "diameter": 5.58, - "x": 113.38, - "y": 11.1, - "z": 74.1 - } - }, - "groups": [ - { - "metadata": {}, - "wells": [ - "A1", - "B1", - "C1", - "D1", - "E1", - "F1", - "G1", - "H1", - "A2", - "B2", - "C2", - "D2", - "E2", - "F2", - "G2", - "H2", - "A3", - "B3", - "C3", - "D3", - "E3", - "F3", - "G3", - "H3", - "A4", - "B4", - "C4", - "D4", - "E4", - "F4", - "G4", - "H4", - "A5", - "B5", - "C5", - "D5", - "E5", - "F5", - "G5", - "H5", - "A6", - "B6", - "C6", - "D6", - "E6", - "F6", - "G6", - "H6", - "A7", - "B7", - "C7", - "D7", - "E7", - "F7", - "G7", - "H7", - "A8", - "B8", - "C8", - "D8", - "E8", - "F8", - "G8", - "H8", - "A9", - "B9", - "C9", - "D9", - "E9", - "F9", - "G9", - "H9", - "A10", - "B10", - "C10", - "D10", - "E10", - "F10", - "G10", - "H10", - "A11", - "B11", - "C11", - "D11", - "E11", - "F11", - "G11", - "H11", - "A12", - "B12", - "C12", - "D12", - "E12", - "F12", - "G12", - "H12" - ] - } - ], - "parameters": { - "format": "96Standard", - "quirks": [], - "isTiprack": true, - "tipLength": 57.9, - "tipOverlap": 10.5, - "isMagneticModuleCompatible": false, - "loadName": "opentrons_flex_96_tiprack_50ul_adp" - }, - "namespace": "custom_beta", - "version": 1, - "schemaVersion": 2, - "cornerOffsetFromSlot": { - "x": 0, - "y": 0, - "z": 0 - } -} diff --git a/hardware-testing/hardware_testing/liquid_sense/__init__.py b/hardware-testing/hardware_testing/liquid_sense/__init__.py new file mode 100644 index 000000000000..e6b26332d7b8 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense.""" diff --git a/hardware-testing/hardware_testing/liquid_sense/__main__.py b/hardware-testing/hardware_testing/liquid_sense/__main__.py new file mode 100644 index 000000000000..10db70e67c8b --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/__main__.py @@ -0,0 +1,317 @@ +"""Liquid sense testing.""" +import argparse +from dataclasses import dataclass +from json import load as json_load +from pathlib import Path +import subprocess +from time import sleep +import os +from typing import List, Any, Optional +import traceback + +from hardware_testing.opentrons_api import helpers_ot3 +from hardware_testing.gravimetric import helpers, workarounds +from hardware_testing.data.csv_report import CSVReport +from hardware_testing.gravimetric.measurement.record import GravimetricRecorder +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.drivers import ( + asair_sensor, + mitutoyo_digimatic_indicator, + list_ports_and_select, +) +from hardware_testing.data import ( + ui, + create_run_id_and_start_time, + get_git_description, + get_testing_data_directory, +) + +from opentrons.protocol_api import InstrumentContext, ProtocolContext +from opentrons.protocol_engine.types import LabwareOffset + +from hardware_testing.liquid_sense import execute +from .report import build_ls_report, store_config, store_serial_numbers +from .post_process import process_csv_directory + +from hardware_testing.protocols.liquid_sense_lpc import ( + liquid_sense_ot3_p50_single, + liquid_sense_ot3_p50_multi, + liquid_sense_ot3_p1000_single, + liquid_sense_ot3_p1000_multi, + liquid_sense_ot3_p1000_96, +) + +API_LEVEL = "2.18" + +LABWARE_OFFSETS: List[LabwareOffset] = [] + + +LIQUID_SENSE_CFG = { + 50: { + 1: liquid_sense_ot3_p50_single, + 8: liquid_sense_ot3_p50_multi, + }, + 1000: { + 1: liquid_sense_ot3_p1000_single, + 8: liquid_sense_ot3_p1000_multi, + 96: liquid_sense_ot3_p1000_96, + }, +} + +PIPETTE_MODEL_NAME = { + 50: { + 1: "p50_single_flex", + 8: "p50_multi_flex", + }, + 1000: { + 1: "p1000_single_flex", + 8: "p1000_multi_flex", + 96: "p1000_96_flex", + }, +} + + +@dataclass +class RunArgs: + """Common resources across multiple runs.""" + + tip_volumes: List[int] + run_id: str + pipette: InstrumentContext + pipette_tag: str + git_description: str + robot_serial: str + recorder: GravimetricRecorder + pipette_volume: int + pipette_channels: int + name: str + environment_sensor: asair_sensor.AsairSensorBase + trials: int + z_speed: float + return_tip: bool + ctx: ProtocolContext + protocol_cfg: Any + test_report: CSVReport + start_height_offset: float + aspirate: bool + dial_indicator: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] + plunger_speed: bool + trials_before_jog: int + + @classmethod + def _get_protocol_context(cls, args: argparse.Namespace) -> ProtocolContext: + if not args.simulate and not args.skip_labware_offsets: + # getting labware offsets must be done before creating the protocol context + # because it requires the robot-server to be running + ui.print_title("SETUP") + ui.print_info( + "Starting opentrons-robot-server, so we can http GET labware offsets" + ) + LABWARE_OFFSETS.extend(workarounds.http_get_all_labware_offsets()) + ui.print_info(f"found {len(LABWARE_OFFSETS)} offsets:") + for offset in LABWARE_OFFSETS: + ui.print_info(f"\t{offset.createdAt}:") + ui.print_info(f"\t\t{offset.definitionUri}") + ui.print_info(f"\t\t{offset.vector}") + # gather the custom labware (for simulation) + custom_defs = {} + if args.simulate: + labware_dir = Path(__file__).parent.parent / "labware" + custom_def_uris = [ + "radwag_pipette_calibration_vial", + "dial_indicator", + ] + for def_uri in custom_def_uris: + with open(labware_dir / def_uri / "1.json", "r") as f: + custom_def = json_load(f) + custom_defs[def_uri] = custom_def + _ctx = helpers.get_api_context( + API_LEVEL, # type: ignore[attr-defined] + is_simulating=args.simulate, + pipette_left=PIPETTE_MODEL_NAME[args.pipette][args.channels], + extra_labware=custom_defs, + ) + for offset in LABWARE_OFFSETS: + engine = _ctx._core._engine_client._transport._engine # type: ignore[attr-defined] + engine.state_view._labware_store._add_labware_offset(offset) + return _ctx + + @classmethod + def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": + """Build.""" + _ctx = RunArgs._get_protocol_context(args) + robot_serial = helpers._get_robot_serial(_ctx.is_simulating()) + run_id, start_time = create_run_id_and_start_time() + environment_sensor = asair_sensor.BuildAsairSensor( + _ctx.is_simulating() or args.ignore_env + ) + git_description = get_git_description() + protocol_cfg = LIQUID_SENSE_CFG[args.pipette][args.channels] + name = protocol_cfg.metadata["protocolName"] # type: ignore[attr-defined] + ui.print_header("LOAD PIPETTE") + pipette = _ctx.load_instrument( + f"flex_{args.channels}channel_{args.pipette}", "left" + ) + loaded_labwares = _ctx.loaded_labwares + if 12 in loaded_labwares.keys(): + trash = loaded_labwares[12] + else: + trash = _ctx.load_labware("opentrons_1_trash_3200ml_fixed", "A3") + pipette.trash_container = trash + pipette_tag = helpers._get_tag_from_pipette(pipette, False, False) + + if args.trials == 0: + trials = 10 + else: + trials = args.trials + + if args.tip == 0: + if args.pipette == 1000: + tip_volumes: List[int] = [50, 200, 1000] + else: + tip_volumes = [50] + else: + tip_volumes = [args.tip] + + scale = Scale.build(simulate=_ctx.is_simulating() or args.ignore_scale) + recorder: GravimetricRecorder = execute._load_scale( + name, + scale, + run_id, + pipette_tag, + start_time, + _ctx.is_simulating() or args.ignore_scale, + ) + dial: Optional[mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator] = None + if not _ctx.is_simulating() and not args.ignore_dial: + dial_port = list_ports_and_select("Dial Indicator") + dial = mitutoyo_digimatic_indicator.Mitutoyo_Digimatic_Indicator( + port=dial_port + ) + dial.connect() + ui.print_info(f"pipette_tag {pipette_tag}") + report = build_ls_report(name, run_id, trials, tip_volumes) + report.set_tag(name) + # go ahead and store the meta data now + store_serial_numbers( + report, + robot_serial, + pipette_tag, + scale.read_serial_number(), + environment_sensor.get_serial(), + git_description, + ) + + store_config( + report, + name, + args.pipette, + tip_volumes, + trials, + args.plunger_direction, + args.liquid, + protocol_cfg.LABWARE_ON_SCALE, # type: ignore[attr-defined] + args.z_speed, + args.start_height_offset, + ) + return RunArgs( + tip_volumes=tip_volumes, + run_id=run_id, + pipette=pipette, + pipette_tag=pipette_tag, + git_description=git_description, + robot_serial=robot_serial, + recorder=recorder, + pipette_volume=args.pipette, + pipette_channels=args.channels, + name=name, + environment_sensor=environment_sensor, + trials=trials, + z_speed=args.z_speed, + return_tip=args.return_tip, + ctx=_ctx, + protocol_cfg=protocol_cfg, + test_report=report, + start_height_offset=args.start_height_offset, + aspirate=args.plunger_direction == "aspirate", + dial_indicator=dial, + plunger_speed=args.plunger_speed, + trials_before_jog=args.trials_before_jog, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Pipette Testing") + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--pipette", type=int, choices=[50, 1000], required=True) + parser.add_argument("--channels", type=int, choices=[1, 8, 96], default=1) + parser.add_argument("--tip", type=int, choices=[0, 50, 200, 1000], default=0) + parser.add_argument("--trials", type=int, default=0) + parser.add_argument("--return-tip", action="store_true") + parser.add_argument("--skip-labware-offsets", action="store_true") + parser.add_argument( + "--liquid", type=str, choices=["water", "glycerol", "alchohol"], default="water" + ) + parser.add_argument("--z-speed", type=float, default=5) + parser.add_argument( + "--plunger-direction", + type=str, + choices=["aspirate", "dispense"], + default="aspirate", + ) + parser.add_argument("--labware-type", type=str, default="nest_1_reservoir_195ml") + parser.add_argument("--plunger-speed", type=float, default=-1.0) + parser.add_argument("--isolate-plungers", action="store_true") + parser.add_argument("--start-height-offset", type=float, default=0) + parser.add_argument("--ignore-scale", action="store_true") + parser.add_argument("--ignore-env", action="store_true") + parser.add_argument("--ignore-dial", action="store_true") + parser.add_argument("--trials-before-jog", type=int, default=10) + + args = parser.parse_args() + run_args = RunArgs.build_run_args(args) + try: + if not run_args.ctx.is_simulating(): + data_dir = get_testing_data_directory() + data_file = f"/{data_dir}/{run_args.name}/{run_args.run_id}/serial.log" + ui.print_info(f"logging can data to {data_file}") + serial_logger = subprocess.Popen( + [f"python3 -m opentrons_hardware.scripts.can_mon > {data_file}"], + shell=True, + ) + sleep(1) + hw = run_args.ctx._core.get_hardware() + if not run_args.ctx.is_simulating(): + ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") + ui.print_info("homing...") + run_args.ctx.home() + for tip in run_args.tip_volumes: + if args.channels == 96 and not run_args.ctx.is_simulating(): + ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) + execute.run(tip, run_args) + except Exception as e: + ui.print_info(f"got error {e}") + ui.print_info(traceback.format_exc()) + finally: + if run_args.recorder is not None: + ui.print_info("ending recording") + run_args.recorder.stop() + run_args.recorder.deactivate() + if not run_args.ctx.is_simulating(): + ui.print_info("killing serial log") + serial_logger.terminate() + if run_args.dial_indicator is not None: + run_args.dial_indicator.disconnect() + run_args.test_report.save_to_disk() + run_args.test_report.print_results() + ui.print_info("done\n\n") + if not run_args.ctx.is_simulating(): + process_csv_directory( + f"{data_dir}/{run_args.name}/{run_args.run_id}", + run_args.tip_volumes, + run_args.trials, + ) + run_args.ctx.cleanup() + if not args.simulate: + helpers_ot3.restart_server_ot3() + os._exit(os.EX_OK) diff --git a/hardware-testing/hardware_testing/liquid_sense/execute.py b/hardware-testing/hardware_testing/liquid_sense/execute.py new file mode 100644 index 000000000000..1fc95d62d44d --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/execute.py @@ -0,0 +1,307 @@ +"""Logic for running a single liquid probe test.""" +from typing import Dict, Any, List, Tuple, Optional +from .report import store_tip_results, store_trial, store_baseline_trial +from opentrons.config.types import LiquidProbeSettings, OutputOptions +from .__main__ import RunArgs +from hardware_testing.gravimetric.workarounds import get_sync_hw_api +from hardware_testing.gravimetric.helpers import ( + _jog_to_find_liquid_height, +) +from hardware_testing.gravimetric.config import LIQUID_PROBE_SETTINGS +from hardware_testing.gravimetric.tips import get_unused_tips +from hardware_testing.data import ui, get_testing_data_directory +from opentrons.hardware_control.types import ( + InstrumentProbeType, + OT3Mount, + Axis, + top_types, +) + +from hardware_testing.gravimetric.measurement.scale import Scale +from hardware_testing.gravimetric.measurement.record import ( + GravimetricRecorder, + GravimetricRecorderConfig, +) +from opentrons.protocol_api._types import OffDeckType + +from opentrons.protocol_api import ProtocolContext, Well, Labware + + +def _load_tipracks( + ctx: ProtocolContext, pipette_channels: int, protocol_cfg: Any, tip: int +) -> List[Labware]: + # TODO add logic here for partial tip using 96 + use_adapters: bool = pipette_channels == 96 + tiprack_load_settings: List[Tuple[int, str]] = [ + ( + slot, + f"opentrons_flex_96_tiprack_{tip}ul", + ) + for slot in protocol_cfg.SLOTS_TIPRACK[tip] # type: ignore[attr-defined] + ] + for ls in tiprack_load_settings: + ui.print_info(f'Loading tiprack "{ls[1]}" in slot #{ls[0]}') + + adapter: Optional[str] = ( + "opentrons_flex_96_tiprack_adapter" if use_adapters else None + ) + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = ctx.loaded_labwares + ui.print_info(f"Loaded labwares {loaded_labwares}") + pre_loaded_tips: List[Labware] = [] + for ls in tiprack_load_settings: + if ls[0] in loaded_labwares.keys(): + if loaded_labwares[ls[0]].name == ls[1]: + pre_loaded_tips.append(loaded_labwares[ls[0]]) + else: + # If something is in the slot that's not what we want, remove it + # we use this only for the 96 channel + ui.print_info( + f"Removing {loaded_labwares[ls[0]].name} from slot {ls[0]}" + ) + ctx._core.move_labware( + loaded_labwares[ls[0]]._core, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + pause_for_manual_move=False, + pick_up_offset=None, + drop_offset=None, + ) + if len(pre_loaded_tips) == len(tiprack_load_settings): + return pre_loaded_tips + + tipracks: List[Labware] = [] + for ls in tiprack_load_settings: + if ctx.deck[ls[0]] is not None: + tipracks.append( + ctx.deck[ls[0]].load_labware(ls[1]) # type: ignore[union-attr] + ) + else: + tipracks.append(ctx.load_labware(ls[1], location=ls[0], adapter=adapter)) + return tipracks + + +def _load_dial_indicator(run_args: RunArgs) -> Labware: + slot_dial = run_args.protocol_cfg.SLOT_DIAL # type: ignore[union-attr] + dial_labware_name = "dial_indicator" + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_dial in loaded_labwares.keys() + and loaded_labwares[slot_dial].name == dial_labware_name + ): + return loaded_labwares[slot_dial] + + dial_labware = run_args.ctx.load_labware( + dial_labware_name, location=slot_dial, namespace="custom_beta" + ) + return dial_labware + + +def _load_test_well(run_args: RunArgs) -> Labware: + slot_scale = run_args.protocol_cfg.SLOT_SCALE # type: ignore[union-attr] + labware_on_scale = run_args.protocol_cfg.LABWARE_ON_SCALE # type: ignore[union-attr] + ui.print_info(f'Loading labware on scale: "{labware_on_scale}"') + if labware_on_scale == "radwag_pipette_calibration_vial": + namespace = "custom_beta" + else: + namespace = "opentrons" + # If running multiple tests in one run, the labware may already be loaded + loaded_labwares = run_args.ctx.loaded_labwares + if ( + slot_scale in loaded_labwares.keys() + and loaded_labwares[slot_scale].name == labware_on_scale + ): + return loaded_labwares[slot_scale] + + labware_on_scale = run_args.ctx.load_labware( + labware_on_scale, location=slot_scale, namespace=namespace + ) + return labware_on_scale + + +def _load_scale( + name: str, + scale: Scale, + run_id: str, + pipette_tag: str, + start_time: float, + simulating: bool, +) -> GravimetricRecorder: + ui.print_header("LOAD SCALE") + ui.print_info( + "Some Radwag settings cannot be controlled remotely.\n" + "Listed below are the things the must be done using the touchscreen:\n" + " 1) Set profile to USER\n" + " 2) Set screensaver to NONE\n" + ) + recorder = GravimetricRecorder( + GravimetricRecorderConfig( + test_name=name, + run_id=run_id, + tag=pipette_tag, + start_time=start_time, + duration=0, + frequency=1000 if simulating else 60, + stable=False, + ), + scale, + simulate=simulating, + start_graph=False, + ) + ui.print_info(f'found scale "{recorder.serial_number}"') + if simulating: + recorder.set_simulation_mass(0) + recorder.record(in_thread=True) + ui.print_info(f'scale is recording to "{recorder.file_name}"') + return recorder + + +def run(tip: int, run_args: RunArgs) -> None: + """Run a liquid probe test.""" + test_labware: Labware = _load_test_well(run_args) + dial_indicator: Labware = _load_dial_indicator(run_args) + dial_well: Well = dial_indicator["A1"] + hw_api = get_sync_hw_api(run_args.ctx) + test_well: Well = test_labware["A1"] + _load_tipracks(run_args.ctx, run_args.pipette_channels, run_args.protocol_cfg, tip) + tips: List[Well] = get_unused_tips( + ctx=run_args.ctx, tip_volume=tip, pipette_mount="" + ) + assert len(tips) >= run_args.trials + results: List[float] = [] + adjusted_results: List[float] = [] + lpc_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + lpc_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + + def _get_baseline() -> float: + run_args.pipette.pick_up_tip(tips.pop(0)) + liquid_height = _jog_to_find_liquid_height( + run_args.ctx, run_args.pipette, test_well + ) + target_height = test_well.bottom(liquid_height).point.z + + run_args.pipette._retract() + # tip_offset = 0.0 + if run_args.dial_indicator is not None: + run_args.pipette.move_to(dial_well.top()) + tip_offset = run_args.dial_indicator.read_stable() + run_args.pipette._retract() + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + + env_data = run_args.environment_sensor.get_reading() + + store_baseline_trial( + run_args.test_report, + tip, + target_height, + env_data.relative_humidity, + env_data.temperature, + test_well.top().point.z - target_height, + tip_offset - lpc_offset, + ) + return target_height + + trials_before_jog = run_args.trials_before_jog + tip_offset = 0.0 + for trial in range(run_args.trials): + if trial % trials_before_jog == 0: + tip_offset = _get_baseline() + + ui.print_info(f"Picking up {tip}ul tip") + run_args.pipette.pick_up_tip(tips.pop(0)) + run_args.pipette.move_to(test_well.top()) + + start_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + height = _run_trial(run_args, tip, test_well, trial) + end_pos = hw_api.current_position_ot3(OT3Mount.LEFT) + run_args.pipette.blow_out() + tip_length_offset = 0.0 + if run_args.dial_indicator is not None: + + run_args.pipette._retract() + run_args.pipette.move_to(dial_well.top()) + tip_length_offset = tip_offset - run_args.dial_indicator.read_stable() + run_args.pipette._retract() + ui.print_info(f"Tip Offset {tip_length_offset}") + + ui.print_info("Droping tip") + if run_args.return_tip: + run_args.pipette.return_tip() + else: + run_args.pipette.drop_tip() + results.append(height) + adjusted_results.append(height + tip_length_offset) + env_data = run_args.environment_sensor.get_reading() + hw_pipette = hw_api.hardware_pipettes[top_types.Mount.LEFT] + plunger_start = ( + hw_pipette.plunger_positions.bottom + if run_args.aspirate + else hw_pipette.plunger_positions.top + ) + store_trial( + run_args.test_report, + trial, + tip, + height, + end_pos[Axis.P_L], + env_data.relative_humidity, + env_data.temperature, + start_pos[Axis.Z_L] - end_pos[Axis.Z_L], + plunger_start - end_pos[Axis.P_L], + tip_length_offset, + ) + ui.print_info( + f"\n\n Z axis start pos {start_pos[Axis.Z_L]} end pos {end_pos[Axis.Z_L]}" + ) + ui.print_info( + f"plunger start pos {plunger_start} end pos {end_pos[Axis.P_L]}\n\n" + ) + + ui.print_info(f"RESULTS: \n{results}") + ui.print_info(f"Adjusted RESULTS: \n{adjusted_results}") + store_tip_results(run_args.test_report, tip, results, adjusted_results) + + +def _run_trial(run_args: RunArgs, tip: int, well: Well, trial: int) -> float: + hw_api = get_sync_hw_api(run_args.ctx) + lqid_cfg: Dict[str, int] = LIQUID_PROBE_SETTINGS[run_args.pipette_volume][ + run_args.pipette_channels + ][tip] + data_dir = get_testing_data_directory() + data_filename = f"pressure_sensor_data-trial{trial}-tip{tip}.csv" + data_file = f"{data_dir}/{run_args.name}/{run_args.run_id}/{data_filename}" + ui.print_info(f"logging pressure data to {data_file}") + + plunger_speed = ( + lqid_cfg["plunger_speed"] + if run_args.plunger_speed == -1 + else run_args.plunger_speed + ) + lps = LiquidProbeSettings( + starting_mount_height=well.top().point.z + run_args.start_height_offset, + max_z_distance=min(well.depth, lqid_cfg["max_z_distance"]), + min_z_distance=lqid_cfg["min_z_distance"], + mount_speed=run_args.z_speed, + plunger_speed=plunger_speed, + sensor_threshold_pascals=lqid_cfg["sensor_threshold_pascals"], + expected_liquid_height=110, + output_option=OutputOptions.sync_buffer_to_csv, + aspirate_while_sensing=run_args.aspirate, + auto_zero_sensor=True, + num_baseline_reads=10, + data_file=data_file, + ) + + hw_mount = OT3Mount.LEFT if run_args.pipette.mount == "left" else OT3Mount.RIGHT + run_args.recorder.set_sample_tag(f"trial-{trial}-{tip}ul") + # TODO add in stuff for secondary probe + height = hw_api.liquid_probe(hw_mount, lps, InstrumentProbeType.PRIMARY) + ui.print_info(f"Trial {trial} complete") + run_args.recorder.clear_sample_tag() + return height diff --git a/hardware-testing/hardware_testing/liquid_sense/post_process.py b/hardware-testing/hardware_testing/liquid_sense/post_process.py new file mode 100644 index 000000000000..20e46ed746a2 --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/post_process.py @@ -0,0 +1,170 @@ +"""Post process script csvs.""" +import csv +import os +from typing import List, Dict, Tuple +from math import isclose + +COL_TRIAL_CONVERSION = { + 1: "E", + 2: "H", + 3: "K", + 4: "N", + 5: "Q", + 6: "T", + 7: "W", + 8: "Z", + 9: "AC", + 10: "AF", + 11: "AI", + 12: "AL", + 13: "AO", +} + + +def process_csv_directory( # noqa: C901 + data_directory: str, tips: List[int], trials: int, make_graph: bool = False +) -> None: + """Post process script csvs.""" + csv_files: List[str] = os.listdir(data_directory) + summary: str = [f for f in csv_files if "CSVReport" in f][0] + final_report_file: str = f"{data_directory}/final_report.csv" + # initialize our data structs + pressure_csvs = [f for f in csv_files if "pressure_sensor_data" in f] + pressure_results_files: Dict[int, List[str]] = {} + pressure_results: Dict[int, Dict[int, List[float]]] = {} + results_settings: Dict[int, Dict[int, Tuple[float, float, float]]] = {} + tip_offsets: Dict[int, List[float]] = {} + p_offsets: Dict[int, List[float]] = {} + meniscus_travel: float = 0 + for tip in tips: + pressure_results_files[tip] = [f for f in pressure_csvs if f"tip{tip}" in f] + pressure_results[tip] = {} + results_settings[tip] = {} + tip_offsets[tip] = [] + p_offsets[tip] = [i * 0 for i in range(trials)] + for trial in range(trials): + pressure_results[tip][trial] = [] + results_settings[tip][trial] = (0.0, 0.0, 0.0) + max_results_len = 0 + + # read in all of the pressure csvs into one big struct so we can process them + for tip in tips: + for trial in range(trials): + with open( + f"{data_directory}/{pressure_results_files[tip][trial]}", newline="" + ) as trial_csv: + trial_reader = csv.reader(trial_csv) + i = 0 + for row in trial_reader: + if i == 1: + results_settings[tip][trial] = ( + float(row[2]), + float(row[3]), + float(row[4]), + ) + if i > 1: + pressure_results[tip][trial].append(float(row[1])) + i += 1 + max_results_len = max([i - 2, max_results_len]) + # start writing the final report csv + with open(f"{data_directory}/{summary}", newline="") as summary_csv: + summary_reader = csv.reader(summary_csv) + with open(final_report_file, "w", newline="") as final_report: + # copy over the results summary + final_report_writer = csv.writer(final_report) + s = 0 + for row in summary_reader: + final_report_writer.writerow(row) + s += 1 + if s == 45: + meniscus_travel = float(row[6]) + if s >= 46 and s < 46 + (trials * len(tips)): + # while processing this grab the tip offsets from the summary + tip_offsets[tips[int((s - 46) / trials)]].append(float(row[8])) + # summary_reader.line_num is the last line in the summary that has text + pressures_start_line = summary_reader.line_num + 3 + # calculate where the start and end of each block of data we want to graph + final_report_writer.writerow( + [ + "50ul", + f"A{pressures_start_line-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + max_results_len -1}", + "200ul", + f"A{pressures_start_line+max_results_len-1}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line +(2*max_results_len)-1}", + "10000ul", + f"A{pressures_start_line+(2*max_results_len-1)}", + f"{COL_TRIAL_CONVERSION[trials]}{pressures_start_line + (3*max_results_len)-1}", + ] + ) + + # build a header row + pressure_header_row = ["time", ""] + for i in range(trials): + pressure_header_row.extend( + [f"pressure T{i+1}", f"z_travel T{i+1}", f"p_travel T{i+1}"] + ) + + # we want to line up the z height's of each trial at time==0 + # to do this we drop the results at the beginning of each of the trials + # except for one with the longest tip (lower tip offset are longer tips) + min_tip_offset = 0.0 + if make_graph: + for tip in tips: + min_tip_offset = min(tip_offsets[tip]) + for trial in range(trials): + for i in range(max_results_len): + if tip_offsets[tip][trial] > min_tip_offset: + # drop this pressure result + pressure_results[tip][trial].pop(0) + # we don't want to change the length of this array so just + # stretch out the last value + pressure_results[tip][trial].append( + pressure_results[tip][trial][-1] + ) + # decrement the offset while this is true + # so we can account for it later + tip_offsets[tip][trial] -= ( + 0.001 * results_settings[tip][0][0] + ) + # keep track of how this effects the plunger start position + p_offsets[tip][trial] = ( + (i + 1) * 0.001 * results_settings[tip][0][1] * -1 + ) + else: + # we've lined up this trial so move to the next + break + # write the processed test data + for tip in tips: + time = 0.0 + final_report_writer.writerow(pressure_header_row) + meniscus_time = (meniscus_travel + min_tip_offset) / results_settings[ + tip + ][0][0] + for i in range(max_results_len): + pressure_row: List[str] = [f"{time}"] + if isclose( + time, + meniscus_time, + rel_tol=0.001, + ): + pressure_row.append("Meniscus") + else: + pressure_row.append("") + for trial in range(trials): + if i < len(pressure_results[tip][trial]): + pressure_row.append(f"{pressure_results[tip][trial][i]}") + else: + pressure_row.append("") + pressure_row.append( + f"{results_settings[tip][trial][0] * time - tip_offsets[tip][trial]}" + ) + pressure_row.append( + f"{abs(results_settings[tip][trial][1]) * time + p_offsets[tip][trial]}" + ) + final_report_writer.writerow(pressure_row) + time += 0.001 + + +if __name__ == "__main__": + process_csv_directory("/home/ryan/testdata", [50], 10) diff --git a/hardware-testing/hardware_testing/liquid_sense/report.py b/hardware-testing/hardware_testing/liquid_sense/report.py new file mode 100644 index 000000000000..bca898e79c7c --- /dev/null +++ b/hardware-testing/hardware_testing/liquid_sense/report.py @@ -0,0 +1,263 @@ +"""Format the csv report for a liquid-sense run.""" + +import statistics +from hardware_testing.data.csv_report import ( + CSVReport, + CSVSection, + CSVLine, + CSVLineRepeating, +) +from typing import List, Union + +""" +CSV Test Report: + - Serial numbers: + - Robot + - Pipette + - Scale + - Environment sensor + - Config: + - protocol name + - pipette_volume + - pipette_mount + - tip_volume + - trials + - plunger direction + - liquid + - labware type + - speed + - start height offset + - Trials + trial-x-{tipsize}ul + - Results + {tipsize}ul-average + {tipsize}ul-cv + {tipsize}ul-d +""" + + +def build_serial_number_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="SERIAL-NUMBERS", + lines=[ + CSVLine("robot", [str]), + CSVLine("git_description", [str]), + CSVLine("pipette", [str]), + CSVLine("scale", [str]), + CSVLine("environment", [str]), + ], + ) + + +def build_config_section() -> CSVSection: + """Build section.""" + return CSVSection( + title="CONFIG", + lines=[ + CSVLine("protocol_name", [str]), + CSVLine("pipette_volume", [str]), + CSVLine("tip_volume", [bool, bool, bool]), + CSVLine("trials", [str]), + CSVLine("plunger_direction", [str]), + CSVLine("liquid", [str]), + CSVLine("labware_type", [str]), + CSVLine("speed", [str]), + CSVLine("start_height_offset", [str]), + ], + ) + + +def build_trials_section(trials: int, tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[Union[CSVLine, CSVLineRepeating]] = [ + CSVLine("trial_number", [str, str, str, str, str, str, str, str]) + ] + lines.extend( + [ + CSVLine( + f"trial-baseline-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + ] + ) + lines.extend( + [ + CSVLine( + f"trial-{t + 1}-{tip}ul", + [float, float, float, float, float, float, float, float], + ) + for tip in tips + for t in range(trials) + ] + ) + + return CSVSection( + title="TRIALS", + lines=lines, + ) + + +def build_results_section(tips: List[int]) -> CSVSection: + """Build section.""" + lines: List[CSVLine] = [] + for tip in tips: + lines.append(CSVLine(f"{tip}ul-average", [float])) + lines.append(CSVLine(f"{tip}ul-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-stdev", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-average", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-minumum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-maximum", [float])) + lines.append(CSVLine(f"{tip}ul-adjusted-stdev", [float])) + return CSVSection(title="RESULTS", lines=lines) # type: ignore[arg-type] + + +def store_serial_numbers( + report: CSVReport, + robot: str, + pipette: str, + scale: str, + environment: str, + git_description: str, +) -> None: + """Report serial numbers.""" + report("SERIAL-NUMBERS", "robot", [robot]) + report("SERIAL-NUMBERS", "git_description", [git_description]) + report("SERIAL-NUMBERS", "pipette", [pipette]) + report("SERIAL-NUMBERS", "scale", [scale]) + report("SERIAL-NUMBERS", "environment", [environment]) + + +def store_config( + report: CSVReport, + protocol_name: str, + pipette_volume: str, + tip_volumes: List[int], + trials: int, + plunger_direction: str, + liquid: str, + labware_type: str, + speed: str, + start_height_offset: str, +) -> None: + """Report config.""" + report("CONFIG", "protocol_name", [protocol_name]) + report("CONFIG", "pipette_volume", [pipette_volume]) + report( + "CONFIG", + "tip_volume", + [50 in tip_volumes, 200 in tip_volumes, 1000 in tip_volumes], + ) + report("CONFIG", "trials", [trials]) + report("CONFIG", "plunger_direction", [plunger_direction]) + report("CONFIG", "liquid", [liquid]) + report("CONFIG", "labware_type", [labware_type]) + report("CONFIG", "speed", [speed]) + report("CONFIG", "start_height_offset", [start_height_offset]) + + +def store_baseline_trial( + report: CSVReport, + tip: float, + height: float, + humidity: float, + temp: float, + z_travel: float, + measured_error: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-baseline-{tip}ul", + [ + height, + 0, + humidity, + temp, + z_travel, + 0, + 0, + measured_error, + ], + ) + + +def store_trial( + report: CSVReport, + trial: int, + tip: float, + height: float, + plunger_pos: float, + humidity: float, + temp: float, + z_travel: float, + plunger_travel: float, + tip_length_offset: float, +) -> None: + """Report Trial.""" + report( + "TRIALS", + f"trial-{trial + 1}-{tip}ul", + [ + height, + plunger_pos, + humidity, + temp, + z_travel, + plunger_travel, + tip_length_offset, + height + tip_length_offset, + ], + ) + + +def store_tip_results( + report: CSVReport, tip: float, results: List[float], adjusted_results: List[float] +) -> None: + """Store final results.""" + report("RESULTS", f"{tip}ul-average", [sum(results) / len(results)]) + report("RESULTS", f"{tip}ul-minumum", [min(results)]) + report("RESULTS", f"{tip}ul-maximum", [max(results)]) + report("RESULTS", f"{tip}ul-stdev", [statistics.stdev(results)]) + report( + "RESULTS", + f"{tip}ul-adjusted-average", + [sum(adjusted_results) / len(adjusted_results)], + ) + report("RESULTS", f"{tip}ul-adjusted-minumum", [min(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-maximum", [max(adjusted_results)]) + report("RESULTS", f"{tip}ul-adjusted-stdev", [statistics.stdev(adjusted_results)]) + + +def build_ls_report( + test_name: str, run_id: str, trials: int, tips: List[int] +) -> CSVReport: + """Generate a CSV Report.""" + report = CSVReport( + test_name=test_name, + sections=[ + build_serial_number_section(), + build_config_section(), + build_trials_section(trials, tips), + build_results_section(tips), + ], + run_id=run_id, + start_time=0.0, + ) + report( + "TRIALS", + "trial_number", + [ + "height", + "plunger_pos", + "humidity", + "temp", + "z_travel", + "plunger_travel", + "tip_length_offset", + "adjusted_height", + ], + ) + return report diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 4beae74bdd93..d1ff8f91d536 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -84,9 +84,7 @@ def stop_server_ot3() -> None: def restart_server_ot3() -> None: """Start opentrons-robot-server on the OT3.""" print('Starting "opentrons-robot-server"...') - Popen( - ["systemctl", "restart", "opentrons-robot-server", "&"], - ) + Popen(["systemctl restart opentrons-robot-server &"], shell=True) def start_server_ot3() -> None: @@ -113,7 +111,7 @@ def _create_fake_pipette_id(mount: OT3Mount, model: Optional[str]) -> Optional[s assert len(items) == 3 size = "P1K" if items[0] == "p1000" else "P50" channels = "S" if items[1] == "single" else "M" - version = items[2].upper().replace(".", "") + version = 35 # model names don't have a version so just fake a 3.5 version date = datetime.now().strftime("%y%m%d") unique_number = 1 if mount == OT3Mount.LEFT else 2 return f"{size}{channels}{version}{date}A0{unique_number}" diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py index e4901928a349..6fe882f53704 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/gravimetric/gravimetric_ot3_p1000_96.py @@ -1,5 +1,6 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "gravimetric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} @@ -8,24 +9,34 @@ SLOTS_TIPRACK = { # TODO: add slot 12 when tipracks are disposable 50: [2, 3, 5, 6, 7, 8, 9, 10, 11], - 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration - 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 200: [2, 3, 5, 6, 7, 8, 9, 10, 11], + 1000: [2, 3, 5, 6, 7, 8, 9, 10, 11], } LABWARE_ON_SCALE = "nest_1_reservoir_195ml" def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tip-racks - ] scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, scale_labware["A1"].top()) - pipette.dispense(10, scale_labware["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py index 4be97d86289c..2cb4dcc1daf4 100644 --- a/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py +++ b/hardware-testing/hardware_testing/protocols/gravimetric_lpc/photometric/photometric_ot3_p1000_96.py @@ -1,12 +1,13 @@ """Photometric OT3 P1000.""" from opentrons.protocol_api import ProtocolContext +from opentrons.protocol_api._types import OffDeckType metadata = {"protocolName": "photometric-ot3-p1000-96"} requirements = {"robotType": "Flex", "apiLevel": "2.15"} SLOTS_TIPRACK = { 50: [5, 6, 8, 9, 11], - 200: [5, 6, 8, 9, 11], # NOTE: ignoring this tip-rack during run() method + 200: [5, 6, 8, 9, 11], } SLOT_PLATE = 3 SLOT_RESERVOIR = 2 @@ -17,20 +18,27 @@ def run(ctx: ProtocolContext) -> None: """Run.""" - tipracks = [ - # FIXME: use official tip-racks once available - ctx.load_labware( - f"opentrons_flex_96_tiprack_{size}uL_adp", slot, namespace="custom_beta" - ) - for size, slots in SLOTS_TIPRACK.items() - for slot in slots - if size == 50 # only calibrate 50ul tips for 96ch test - ] reservoir = ctx.load_labware(RESERVOIR_LABWARE, SLOT_RESERVOIR) plate = ctx.load_labware(PHOTOPLATE_LABWARE, SLOT_PLATE) pipette = ctx.load_instrument("flex_96channel_1000", "left") - for rack in tipracks: - pipette.pick_up_tip(rack["A1"]) - pipette.aspirate(10, reservoir["A1"].top()) - pipette.dispense(10, plate["A1"].top()) - pipette.drop_tip(home_after=False) + adapters = [ + ctx.load_adapter("opentrons_flex_96_tiprack_adapter", slot) + for slot in SLOTS_TIPRACK[50] + ] + for tip_size in SLOTS_TIPRACK.keys(): + tipracks = [ + adapter.load_labware(f"opentrons_flex_96_tiprack_{tip_size}uL") + for adapter in adapters + ] + for rack in tipracks: + pipette.pick_up_tip(rack) + pipette.aspirate(10, reservoir["A1"].top()) + pipette.dispense(10, plate["A1"].top()) + pipette.drop_tip(home_after=False) + + for rack in tipracks: + ctx.move_labware( + rack, + new_location=OffDeckType.OFF_DECK, + use_gripper=False, + ) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py new file mode 100644 index 000000000000..6ec34e45de01 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/__init__.py @@ -0,0 +1 @@ +"""Liquid Sense LPC.""" diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py new file mode 100644 index 000000000000..02644b314a48 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_96.py @@ -0,0 +1,33 @@ +"""Liquid sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-96"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + # TODO: add slot 12 when tipracks are disposable + 50: [2, 3, 6, 7, 8, 9, 10, 11], + 200: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration + 1000: [2, 3, 6, 7, 8, 9, 10, 11], # NOTE: ignored during calibration +} + +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL_adp", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + if size == 50 # only calibrate 50ul tip-racks + ] + scale_labware = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("p1000_96", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, scale_labware["A1"].top()) + pipette.dispense(10, scale_labware["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py new file mode 100644 index 000000000000..d2b806d1229b --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_multi.py @@ -0,0 +1,26 @@ +"""LiquidSense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-multi"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = {50: [2], 200: [3], 1000: [6]} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py new file mode 100644 index 000000000000..4e8fcc177f4f --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p1000_single.py @@ -0,0 +1,33 @@ +"""Liquid Sense OT3 P1000.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p1000-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], + 200: [6], + 1000: [9], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_1000", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py new file mode 100644 index 000000000000..34f83cd4cf77 --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_multi.py @@ -0,0 +1,28 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid_sense-ot3-p50-multi-50ul-tip"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "nest_1_reservoir_195ml" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + pipette = ctx.load_instrument("flex_8channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(pipette.min_volume, vial["A1"].top()) + pipette.dispense(pipette.min_volume, vial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py new file mode 100644 index 000000000000..8e9d65a72e2c --- /dev/null +++ b/hardware-testing/hardware_testing/protocols/liquid_sense_lpc/liquid_sense_ot3_p50_single.py @@ -0,0 +1,31 @@ +"""Liquid Sense OT3.""" +from opentrons.protocol_api import ProtocolContext + +metadata = {"protocolName": "liquid-sense-ot3-p50-single"} +requirements = {"robotType": "Flex", "apiLevel": "2.15"} + +SLOT_SCALE = 4 +SLOT_DIAL = 5 +SLOTS_TIPRACK = { + 50: [3], +} +LABWARE_ON_SCALE = "radwag_pipette_calibration_vial" + + +def run(ctx: ProtocolContext) -> None: + """Run.""" + tipracks = [ + ctx.load_labware(f"opentrons_flex_96_tiprack_{size}uL", slot) + for size, slots in SLOTS_TIPRACK.items() + for slot in slots + ] + vial = ctx.load_labware(LABWARE_ON_SCALE, SLOT_SCALE) + dial = ctx.load_labware("dial_indicator", SLOT_DIAL) + pipette = ctx.load_instrument("flex_1channel_50", "left") + for rack in tipracks: + pipette.pick_up_tip(rack["A1"]) + pipette.aspirate(10, vial["A1"].top()) + pipette.dispense(10, vial["A1"].top()) + pipette.aspirate(1, dial["A1"].top()) + pipette.dispense(1, dial["A1"].top()) + pipette.drop_tip(home_after=False) diff --git a/hardware-testing/tests/hardware_testing/liquid/test_heights.py b/hardware-testing/tests/hardware_testing/liquid/test_heights.py index ab73b54618c1..39efb419e65e 100644 --- a/hardware-testing/tests/hardware_testing/liquid/test_heights.py +++ b/hardware-testing/tests/hardware_testing/liquid/test_heights.py @@ -17,7 +17,7 @@ def _create_context() -> ProtocolContext: - return get_api_context(api_level="2.13", is_simulating=True) + return get_api_context(api_level="2.16", is_simulating=True) def _load_labware(ctx: ProtocolContext) -> Tuple[Labware, Labware, Labware, Labware]: diff --git a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py index 49c1584526d8..4c54e5a8632b 100644 --- a/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py +++ b/hardware/opentrons_hardware/drivers/binary_usb/binary_messenger.py @@ -196,7 +196,6 @@ async def _read_task(self) -> None: if filter and not filter( BinaryMessageId(message_definition.message_id.value) ): - log.debug("message ignored by filter") continue listener(message_definition) if ( diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index 4446b3b06833..c0b49e376bb7 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -379,7 +379,6 @@ async def _read_task(self) -> None: handled = False for listener, filter in self._listeners.values(): if filter and not filter(message.arbitration_id): - log.debug("message ignored by filter") continue listener(message_definition(payload=build), message.arbitration_id) # type: ignore[arg-type] handled = True diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py index 6611edecfe4d..9906aa8dc078 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/messages.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/messages.py @@ -111,6 +111,7 @@ defs.GetHepaUVStateResponse, defs.SendAccumulatedPressureDataRequest, defs.AddSensorLinearMoveRequest, + defs.SendAccumulatedPressureDataRequest, ] diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index f4bca8cb881e..c351495ba5bc 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -665,6 +665,7 @@ class GetHepaFanStatePayloadResponse(EmptyPayload): duty_cycle: utils.UInt32Field fan_on: utils.UInt8Field + fan_rpm: utils.UInt16Field @dataclass(eq=False) diff --git a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py index 0716a4f4c90f..2812cdf3f7df 100644 --- a/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py +++ b/hardware/opentrons_hardware/hardware_control/hepa_uv_settings.py @@ -35,6 +35,7 @@ class HepaFanState: fan_on: bool duty_cycle: int + fan_rpm: int @dataclass(frozen=True) @@ -80,6 +81,7 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: fan_state = HepaFanState( fan_on=bool(message.payload.fan_on.value), duty_cycle=int(message.payload.duty_cycle.value), + fan_rpm=int(message.payload.fan_rpm.value), ) def _filter(arb_id: ArbitrationId) -> bool: diff --git a/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py b/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py index 9928b841da97..32897d16679e 100644 --- a/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py +++ b/hardware/opentrons_hardware/hardware_control/motor_enable_disable.py @@ -122,7 +122,9 @@ def _listener(message: MessageDefinition, arb_id: ArbitrationId) -> None: ) else: log.debug("Read motor status terminated, no missing nodes.") - return reported + finally: + can_messenger.remove_listener(_listener) + return reported async def get_tip_motor_enabled( diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 94301464f220..67e85a1554b2 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -201,7 +201,6 @@ async def liquid_probe( csv_output: bool = False, sync_buffer_output: bool = False, can_bus_only_output: bool = False, - # output_option: OutputOptions, data_file: Optional[str] = None, auto_zero_sensor: bool = True, num_baseline_reads: int = 10, @@ -232,7 +231,7 @@ async def liquid_probe( ) sensor_runner = MoveGroupRunner(move_groups=[[sensor_group]]) - log_file: str = "/var/pressure_sensor_data.csv" if not data_file else data_file + log_file: str = "/data/pressure_sensor_data.csv" if not data_file else data_file if csv_output: return await run_stream_output_to_csv( messenger, diff --git a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py index 2401aee34b4c..dcaf85a86536 100644 --- a/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py +++ b/hardware/tests/opentrons_hardware/hardware_control/test_hepauv_settings.py @@ -37,12 +37,15 @@ def mock_can_messenger() -> AsyncMock: return AsyncMock() -def create_hepa_fan_state_response(fan_on: bool, duty_cycle: int) -> MessageDefinition: +def create_hepa_fan_state_response( + fan_on: bool, duty_cycle: int, fan_rpm: int +) -> MessageDefinition: """Create a GetHepaFanStateResponse.""" return md.GetHepaFanStateResponse( payload=GetHepaFanStatePayloadResponse( fan_on=UInt8Field(fan_on), duty_cycle=UInt32Field(duty_cycle), + fan_rpm=UInt16Field(fan_rpm), ) ) @@ -111,10 +114,11 @@ async def test_set_hepa_uv_state( @pytest.mark.parametrize( "response", [ - (NodeId.host, create_hepa_fan_state_response(True, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(True, 0), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 75), NodeId.hepa_uv), - (NodeId.host, create_hepa_fan_state_response(False, 100), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 50, 4540), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 75, 6790), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(True, 0, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 75, 0), NodeId.hepa_uv), + (NodeId.host, create_hepa_fan_state_response(False, 100, 0), NodeId.hepa_uv), ], ) async def test_get_hepa_fan_state( @@ -147,6 +151,7 @@ def responder( HepaFanState( bool(payload.fan_on.value), int(payload.duty_cycle.value), + int(payload.fan_rpm.value), ) == res ) diff --git a/labware-designer/webpack.config.js b/labware-designer/webpack.config.js deleted file mode 100644 index aec3b7cc0cb2..000000000000 --- a/labware-designer/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') - -const { baseConfig } = require('@opentrons/webpack-config') -const { productName: title, description, author } = require('./package.json') - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const OUTPUT_PATH = path.join(__dirname, 'dist') - -module.exports = webpackMerge(baseConfig, { - entry: [JS_ENTRY], - - output: { - path: OUTPUT_PATH, - }, - - plugins: [ - new HtmlWebpackPlugin({ title, description, author, template: HTML_ENTRY }), - new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer' }), - ], -}) diff --git a/labware-library/webpack.config.js b/labware-library/webpack.config.js deleted file mode 100644 index c5fb0d8c7e88..000000000000 --- a/labware-library/webpack.config.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict' -const path = require('path') -const webpack = require('webpack') -const merge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -// const glob = require('glob') - -const { baseConfig } = require('@opentrons/webpack-config') -// const {baseConfig, DEV_MODE} = require('@opentrons/webpack-config') -const pkg = require('./package.json') - -const { versionForProject } = require('../scripts/git-version') - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const OUT_PATH = path.join(__dirname, 'dist') - -const LABWARE_LIBRARY_ENV_VAR_PREFIX = 'OT_LL' - -const passThruEnvVars = Object.keys(process.env) - .filter(v => v.startsWith(LABWARE_LIBRARY_ENV_VAR_PREFIX)) - .concat(['NODE_ENV', 'CYPRESS']) - -const testAliases = - process.env.CYPRESS === '1' - ? { - 'file-saver': path.resolve(__dirname, 'cypress/mocks/file-saver.js'), - } - : {} - -module.exports = async () => { - const envVarsWithDefaults = { - OT_LL_VERSION: await versionForProject('labware-library'), - OT_LL_BUILD_DATE: new Date().toUTCString(), - } - - const envVars = passThruEnvVars.reduce( - (acc, envVar) => ({ [envVar]: '', ...acc }), - { ...envVarsWithDefaults } - ) - - return merge(baseConfig, { - entry: JS_ENTRY, - - output: { - path: OUT_PATH, - publicPath: '/', - }, - - plugins: [ - new webpack.EnvironmentPlugin(envVars), - - new HtmlWebpackPlugin({ - template: HTML_ENTRY, - title: pkg.productName, - description: pkg.description, - author: pkg.author.name, - gtmId: process.env.GTM_ID, - favicon: './src/images/favicon.ico', - }), - ], - - resolve: { - alias: testAliases, - }, - }) -} diff --git a/opentrons-ai-client/Makefile b/opentrons-ai-client/Makefile new file mode 100644 index 000000000000..9c15fa32e41e --- /dev/null +++ b/opentrons-ai-client/Makefile @@ -0,0 +1,59 @@ +# opentrons ai client makefile + +# using bash instead of /bin/bash in SHELL prevents macOS optimizing away our PATH update +SHELL := bash + +# add node_modules/.bin to PATH +PATH := $(shell cd .. && yarn bin):$(PATH) + +benchmark_output := $(shell node -e 'console.log(new Date());') + +# These variables can be overriden when make is invoked to customize the +# behavior of jest +tests ?= +cov_opts ?= --coverage=true +test_opts ?= + +# standard targets +##################################################################### + +.PHONY: all +all: clean build + +.PHONY: setup +setup: + yarn + +.PHONY: clean +clean: + shx rm -rf dist + +# artifacts +##################################################################### + +.PHONY: build +build: export NODE_ENV := production +build: + vite build + git rev-parse HEAD > dist/.commit + +# development +##################################################################### + +.PHONY: dev +dev: export NODE_ENV := development +dev: + vite serve + +# production assets server +.PHONY: serve +serve: all + node ../scripts/serve-static dist + +.PHONY: test +test: + $(MAKE) -C .. test-js-ai-client tests="$(tests)" test_opts="$(test_opts)" + +.PHONY: test-cov +test-cov: + make -C .. test-js-ai-client tests=$(tests) test_opts="$(test_opts)" cov_opts="$(cov_opts)" diff --git a/opentrons-ai-client/README.md b/opentrons-ai-client/README.md new file mode 100644 index 000000000000..d4c80c2bb23b --- /dev/null +++ b/opentrons-ai-client/README.md @@ -0,0 +1,63 @@ +# Opentrons AI Frontend + +[![JavaScript Style Guide][style-guide-badge]][style-guide] + +## Overview + +The Opentrons AI application helps you to create a protocol with natural language. + +## Developing + +To get started: clone the `Opentrons/opentrons` repository, set up your computer for development as specified in the [contributing guide][contributing-guide-setup], and then: + +```shell +# change into the cloned directory +cd opentrons +# prerequisite: install dependencies as specified in project setup +make setup +# launch the dev server +make -C opentrons-ai-client dev +``` + +## Stack and structure + +The UI stack is built using: + +- [React][] +- [Babel][] +- [Vite][] + +Some important directories: + +- [opentrons-ai-server][] — Opentrons AI application's server + +## Copy management + +We use [i18next](https://www.i18next.com) for copy management and internationalization. + +## Testing + +Tests for the Opentrons App are run from the top level along with all other JS project tests. + +- `make test-js` - Run all JavaScript tests + +Test tasks can also be run with the following arguments: + +| Argument | Default | Description | Example | +| -------- | -------- | ----------------------- | --------------------------------- | +| watch | `false` | Run tests in watch mode | `make test-unit watch=true` | +| cover | `!watch` | Calculate code coverage | `make test watch=true cover=true` | + +## Building + +TBD + +[style-guide]: https://standardjs.com +[style-guide-badge]: https://img.shields.io/badge/code_style-standard-brightgreen.svg?style=flat-square&maxAge=3600 +[contributing-guide-setup]: ../CONTRIBUTING.md#development-setup +[contributing-guide-running-the-api]: ../CONTRIBUTING.md#opentrons-api +[react]: https://react.dev/ +[babel]: https://babeljs.io/ +[vite]: https://vitejs.dev/ +[bundle-analyzer]: https://github.com/webpack-contrib/webpack-bundle-analyzer +[opentrons-ai-server]: https://github.com/Opentrons/opentrons/tree/edge/opentrons-ai-server diff --git a/opentrons-ai-client/babel.config.cjs b/opentrons-ai-client/babel.config.cjs new file mode 100644 index 000000000000..11739e6bf000 --- /dev/null +++ b/opentrons-ai-client/babel.config.cjs @@ -0,0 +1,21 @@ +'use strict' + +module.exports = { + env: { + // Must have babel-plugin-styled-components in each env, + // see here for further details: s https://styled-components.com/docs/tooling#babel-plugin + production: { + plugins: ['babel-plugin-styled-components', 'babel-plugin-unassert'], + }, + development: { + plugins: ['babel-plugin-styled-components'], + }, + test: { + plugins: [ + // disable ssr, displayName to fix toHaveStyleRule + // https://github.com/styled-components/jest-styled-components/issues/294 + ['babel-plugin-styled-components', { ssr: false, displayName: false }], + ], + }, + }, +} diff --git a/opentrons-ai-client/index.html b/opentrons-ai-client/index.html new file mode 100644 index 000000000000..57e7f83f5915 --- /dev/null +++ b/opentrons-ai-client/index.html @@ -0,0 +1,13 @@ + + + + + + + Opentrons AI + + +
+ + + diff --git a/opentrons-ai-client/package.json b/opentrons-ai-client/package.json new file mode 100644 index 000000000000..39d4f6d275c9 --- /dev/null +++ b/opentrons-ai-client/package.json @@ -0,0 +1,38 @@ +{ + "name": "opentrons-ai-client", + "type": "module", + "version": "0.0.0-dev", + "description": "Opentrons AI application UI", + "source": "src/index.tsx", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Opentrons/opentrons.git" + }, + "author": { + "name": "Opentrons Labworks", + "email": "engineering@opentrons.com" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/Opentrons/opentrons/issues" + }, + "homepage": "https://github.com/Opentrons/opentrons", + "dependencies": { + "@fontsource/public-sans": "5.0.3", + "@opentrons/components": "link:../components", + "i18next": "^19.8.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-error-boundary": "^4.0.10", + "react-i18next": "13.5.0", + "react-markdown": "9.0.1", + "styled-components": "5.3.6" + }, + "engines": { + "node": ">=18.19.0" + }, + "devDependencies": { + "@types/styled-components": "^5.1.26" + } +} diff --git a/opentrons-ai-client/src/App.test.tsx b/opentrons-ai-client/src/App.test.tsx new file mode 100644 index 000000000000..4ae3494a53c3 --- /dev/null +++ b/opentrons-ai-client/src/App.test.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from './__testing-utils__' +import { SidePanel } from './molecules/SidePanel' +import { ChatContainer } from './organisms/ChatContainer' + +import { App } from './App' + +vi.mock('./molecules/SidePanel') +vi.mock('./organisms/ChatContainer') + +const render = (): ReturnType => { + return renderWithProviders() +} + +describe('App', () => { + beforeEach(() => { + vi.mocked(SidePanel).mockReturnValue(
mock SidePanel
) + vi.mocked(ChatContainer).mockReturnValue(
mock ChatContainer
) + }) + + it('should render text', () => { + render() + screen.getByText('mock SidePanel') + screen.getByText('mock ChatContainer') + }) +}) diff --git a/opentrons-ai-client/src/App.tsx b/opentrons-ai-client/src/App.tsx new file mode 100644 index 000000000000..268a61b2e7f8 --- /dev/null +++ b/opentrons-ai-client/src/App.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import { DIRECTION_ROW, Flex } from '@opentrons/components' + +import { SidePanel } from './molecules/SidePanel' +import { ChatContainer } from './organisms/ChatContainer' + +export function App(): JSX.Element { + return ( + + + + + ) +} diff --git a/opentrons-ai-client/src/__testing-utils__/index.ts b/opentrons-ai-client/src/__testing-utils__/index.ts new file mode 100644 index 000000000000..e17c0ffbc315 --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/index.ts @@ -0,0 +1,2 @@ +export * from './renderWithProviders' +export * from './matchers' diff --git a/opentrons-ai-client/src/__testing-utils__/matchers.ts b/opentrons-ai-client/src/__testing-utils__/matchers.ts new file mode 100644 index 000000000000..66234dbc9152 --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/matchers.ts @@ -0,0 +1,24 @@ +import type { Matcher } from '@testing-library/react' + +// Match things like

Some nested text

+// Use with either string match: getByText(nestedTextMatcher("Some nested text")) +// or regexp: getByText(nestedTextMatcher(/Some nested text/)) +export const nestedTextMatcher = (textMatch: string | RegExp): Matcher => ( + content, + node +) => { + const hasText = (n: typeof node): boolean => { + if (n == null || n.textContent === null) return false + return typeof textMatch === 'string' + ? Boolean(n?.textContent.match(textMatch)) + : textMatch.test(n.textContent) + } + const nodeHasText = hasText(node) + const childrenDontHaveText = + node != null && Array.from(node.children).every(child => !hasText(child)) + + return nodeHasText && childrenDontHaveText +} + +// need componentPropsMatcher +// need partialComponentPropsMatcher diff --git a/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx new file mode 100644 index 000000000000..65a2e01855e4 --- /dev/null +++ b/opentrons-ai-client/src/__testing-utils__/renderWithProviders.tsx @@ -0,0 +1,53 @@ +// render using targetted component using @testing-library/react +// with wrapping providers for i18next and redux +import * as React from 'react' +import { QueryClient, QueryClientProvider } from 'react-query' +import { I18nextProvider } from 'react-i18next' +import { Provider } from 'react-redux' +import { vi } from 'vitest' +import { render } from '@testing-library/react' +import { createStore } from 'redux' + +import type { PreloadedState, Store } from 'redux' +import type { RenderOptions, RenderResult } from '@testing-library/react' + +export interface RenderWithProvidersOptions extends RenderOptions { + initialState?: State + i18nInstance: React.ComponentProps['i18n'] +} + +export function renderWithProviders( + Component: React.ReactElement, + options?: RenderWithProvidersOptions +): [RenderResult, Store] { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const { initialState = {}, i18nInstance = null } = options || {} + + const store: Store = createStore( + vi.fn(), + initialState as PreloadedState + ) + store.dispatch = vi.fn() + store.getState = vi.fn(() => initialState) as () => State + + const queryClient = new QueryClient() + + const ProviderWrapper: React.ComponentType> = ({ + children, + }) => { + const BaseWrapper = ( + + {children} + + ) + if (i18nInstance != null) { + return ( + {BaseWrapper} + ) + } else { + return BaseWrapper + } + } + + return [render(Component, { wrapper: ProviderWrapper }), store] +} diff --git a/opentrons-ai-client/src/assets/images/opentrons_logo.svg b/opentrons-ai-client/src/assets/images/opentrons_logo.svg new file mode 100644 index 000000000000..b183d161e81b --- /dev/null +++ b/opentrons-ai-client/src/assets/images/opentrons_logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opentrons-ai-client/src/assets/localization/en/index.ts b/opentrons-ai-client/src/assets/localization/en/index.ts new file mode 100644 index 000000000000..b5aa26621ddf --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/index.ts @@ -0,0 +1,7 @@ +import shared from './shared.json' +import protocol_generator from './protocol_generator.json' + +export const en = { + shared, + protocol_generator, +} diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json new file mode 100644 index 000000000000..80d273abffe7 --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -0,0 +1,27 @@ +{ + "api": "API: An API level is 2.15", + "application": "Application: Your protocol's name, describing what it does.", + "commands": "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations.", + "got_feedback": "Got feedback? We love to hear it.", + "make_sure_your_prompt": "Make sure your prompt includes the following:", + "metadata": "Metadata: Three pieces of information.", + "modules": "Modules: Thermocycler or Temperature Module.", + "opentronsai_asks": "OpentronsAI asks you to provide it!", + "opentronsai": "OpentronsAI", + "ot2_pipettes": "OT-2 pipettes: Include volume, number of channels, and generation.", + "prc_flex": "PCR (Flex)", + "prc": "PCR", + "reagent_transfer_flex": "Reagent Transfer (Flex)", + "reagent_transfer": "Reagent Transfer", + "robot": "Robot: OT-2.", + "share_your_thoughts": "Share your thoughts here", + "side_panel_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", + "side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", + "tipracks_and_labware": "Tip racks and labware: Use names from the Opentrons Labware Library.", + "try_example_prompts": "Stuck? Try these example prompts to get started.", + "type_your_prompt": "Type your prompt...", + "well_allocations": "Well allocations: Describe where liquids should go in labware.", + "what_if_you": "What if you don’t provide all of those pieces of information? OpentronsAI asks you to provide it!", + "what_typeof_protocol": "What type of protocol do you need?", + "you": "You" +} diff --git a/opentrons-ai-client/src/assets/localization/en/shared.json b/opentrons-ai-client/src/assets/localization/en/shared.json new file mode 100644 index 000000000000..46cb365873f4 --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/en/shared.json @@ -0,0 +1,3 @@ +{ + "send": "Send" +} diff --git a/opentrons-ai-client/src/assets/localization/index.ts b/opentrons-ai-client/src/assets/localization/index.ts new file mode 100644 index 000000000000..e92a7077ed95 --- /dev/null +++ b/opentrons-ai-client/src/assets/localization/index.ts @@ -0,0 +1,5 @@ +import { en } from './en' + +export const resources = { + en, +} diff --git a/opentrons-ai-client/src/atoms/GlobalStyle/index.ts b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts new file mode 100644 index 000000000000..782a2a0b91be --- /dev/null +++ b/opentrons-ai-client/src/atoms/GlobalStyle/index.ts @@ -0,0 +1,34 @@ +import { createGlobalStyle } from 'styled-components' +import { COLORS } from '@opentrons/components' +import '@fontsource/public-sans' +import '@fontsource/public-sans/600.css' +import '@fontsource/public-sans/700.css' + +export const GlobalStyle = createGlobalStyle` + * { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Public Sans', 'sans-serif'; + } + + html, + body { + width: 100%; + height: 100%; + color: ${COLORS.black90}; + } + + a { + text-decoration: none; + } + + button { + border: none; + + &:focus, + &:active { + outline: 0; + } + } +` diff --git a/opentrons-ai-client/src/i18n.ts b/opentrons-ai-client/src/i18n.ts new file mode 100644 index 000000000000..0f7ef3bf6dfb --- /dev/null +++ b/opentrons-ai-client/src/i18n.ts @@ -0,0 +1,45 @@ +import i18n from 'i18next' +import capitalize from 'lodash/capitalize' +import startCase from 'lodash/startCase' +import { initReactI18next } from 'react-i18next' +import { resources } from './assets/localization' +import { titleCase } from '@opentrons/shared-data' + +i18n.use(initReactI18next).init( + { + resources, + lng: 'en', + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + ns: ['shared'], + defaultNS: 'shared', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format, lng) { + if (format === 'upperCase') return value.toUpperCase() + if (format === 'lowerCase') return value.toLowerCase() + if (format === 'capitalize') return capitalize(value) + if (format === 'sentenceCase') return startCase(value) + if (format === 'titleCase') return titleCase(value) + return value + }, + }, + keySeparator: false, // use namespaces and context instead + saveMissing: true, + missingKeyHandler: (lng, ns, key) => { + process.env.NODE_ENV === 'test' + ? console.error(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + : console.warn(`Missing ${lng} Translation: key={${key}} ns={${ns}}`) + }, + }, + err => { + if (err) { + console.error( + 'Internationalization was not initialized properly. error: ', + err + ) + } + } +) + +export { i18n } diff --git a/opentrons-ai-client/src/main.tsx b/opentrons-ai-client/src/main.tsx new file mode 100644 index 000000000000..a2f1338bd7bb --- /dev/null +++ b/opentrons-ai-client/src/main.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { I18nextProvider } from 'react-i18next' +import { GlobalStyle } from './atoms/GlobalStyle' + +import { i18n } from './i18n' +import { App } from './App' + +const rootElement = document.getElementById('root') +if (rootElement != null) { + ReactDOM.createRoot(rootElement).render( + + + + + + + ) +} else { + console.error('Root element not found') +} diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx new file mode 100644 index 000000000000..ae03a25f7546 --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/ChatDisplay.stories.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { COLORS, Flex, SPACING } from '@opentrons/components' +import { i18n } from '../../i18n' +import { ChatDisplay } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/ChatDisplay', + component: ChatDisplay, + decorators: [ + Story => ( + + + + + + ), + ], +} +export default meta +type Story = StoryObj + +export const OpentronsAI: Story = { + args: { + content: ` +## sample output from OpentronsAI + +\`\`\`py +from opentrons import protocol_api +# Metadata +metadata = { + 'protocolName': 'ThermoPrime Taq DNA Polymerase PCR Amplification', + 'author': 'Name ', + 'description': 'PCR amplification using ThermoPrime Taq DNA Polymerase kit', + 'apiLevel': '2.11' +} + +# Protocol run function +def run(protocol: protocol_api.ProtocolContext): + + # Constants + NO_OF_SAMPLES = 41 + SAMPLE_VOL = 3 # uL + MASTERMIX_VOL = 10 # uL + TC_SAMPLE_MASTERMIX_MIXES = 4 + TC_SAMPLE_MASTERMIX_MIX_VOLUME = SAMPLE_VOL + MASTERMIX_VOL + MASTERMIX_BLOCK_TEMP = 10 # degree C + TEMP_DECK_WAIT_TIME = 50 # seconds +\`\`\` +`, + isUserInput: false, + }, +} + +export const User: Story = { + args: { + content: ` + - Application: Reagent transfer + - Robot: OT-2 + - API: 2.13 + + Pipette mount: + - P1000 Single-Channel GEN2 is mounted on left + - P300 Single-Channel GEN2 is mounted on right + + Labware: + - Source Labware: Thermo Scientific Nunc 96 Well Plate 2000 µL on slot 7 + - Destination Labware: Opentrons 24 Well Aluminum Block with NEST 0.5 mL Screwcap on slot 3 + - Tiprack: Opentrons 96 Filter Tip Rack 1000 µL on slot 4 + + Commands: + - Using P1000 Single-Channel GEN2 pipette on left mount, transfer 195.0 uL of reagent + from H10, F12, D7, B1, C8 wells in source labware + to first well in the destination labware. + Use new tip for each transfer. + `, + isUserInput: true, + }, +} diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx new file mode 100644 index 000000000000..75b99717abb8 --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { ChatDisplay } from '../index' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('ChatDisplay', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + content: 'mock text from the backend', + isUserInput: false, + } + }) + it('should display response from the backend and label', () => { + render(props) + screen.getByText('OpentronsAI') + screen.getByText('mock text from the backend') + // ToDO (kk:04/16/2024) activate the following when jsdom's issue is solved + // const display = screen.getByTextId('ChatDisplay_from_backend') + // expect(display).toHaveStyle(`background-color: ${COLORS.grey30}`) + }) + it('should display input from use and label', () => { + props = { + content: 'mock text from user input', + isUserInput: true, + } + render(props) + screen.getByText('You') + screen.getByText('mock text from user input') + // ToDO (kk:04/16/2024) activate the following when jsdom's issue is solved + // const display = screen.getByTextId('ChatDisplay_from_user') + // expect(display).toHaveStyle(`background-color: ${COLORS.blue}`) + }) +}) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx new file mode 100644 index 000000000000..c2d52e6a5931 --- /dev/null +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import Markdown from 'react-markdown' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' + +interface ChatDisplayProps { + content: string + isUserInput: boolean +} + +export function ChatDisplay({ + content, + isUserInput, +}: ChatDisplayProps): JSX.Element { + const { t } = useTranslation('protocol_generator') + return ( + + {isUserInput ? t('you') : t('opentronsai')} + {/* text should be markdown so this component will have a package or function to parse markdown */} + + {/* ToDo (kk:04/19/2024) I will get feedback for additional styling from the design team. */} + {content} + + + ) +} diff --git a/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx b/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx new file mode 100644 index 000000000000..1a29b80c7093 --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/PromptGuide.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../i18n' +import { PromptGuide as PromptGuideComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/PromptGuide', + component: PromptGuideComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const PromptGuide: Story = {} diff --git a/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx b/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx new file mode 100644 index 000000000000..babe9f271f8b --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/__tests__/PromptGuide.test.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { screen } from '@testing-library/react' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { PromptGuide } from '../index' + +const LABWARE_LIBRARY_URL = 'https://labware.opentrons.com/' + +const render = () => { + return renderWithProviders(, { i18nInstance: i18n }) +} + +describe('PromptGuide', () => { + it('should render text', () => { + render() + screen.getByText('What type of protocol do you need?') + screen.getByText('Make sure your prompt includes the following:') + screen.getByText('Metadata: Three pieces of information.') + screen.getByText( + "Application: Your protocol's name, describing what it does." + ) + screen.getByText('Robot: OT-2.') + screen.getByText('API: An API level is 2.15') + screen.getByText( + 'OT-2 pipettes: Include volume, number of channels, and generation.' + ) + screen.getByText('Modules: Thermocycler or Temperature Module.') + screen.getByText( + 'Well allocations: Describe where liquids should go in labware.' + ) + screen.getByText( + "Commands: List the protocol's steps, specifying quantities in microliters and giving exact source and destination locations." + ) + screen.getByText( + 'What if you don’t provide all of those pieces of information?' + ) + screen.getByText('OpentronsAI asks you to provide it!') + }) + it('should have the right url', () => { + render() + const link = screen.getByRole('link', { name: 'Opentrons Labware Library' }) + expect(link).toHaveAttribute('href', LABWARE_LIBRARY_URL) + }) +}) diff --git a/opentrons-ai-client/src/molecules/PromptGuide/index.tsx b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx new file mode 100644 index 000000000000..16d995d5cfae --- /dev/null +++ b/opentrons-ai-client/src/molecules/PromptGuide/index.tsx @@ -0,0 +1,113 @@ +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Link, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +const LABWARE_LIBRARY_URL = 'https://labware.opentrons.com/' + +export function PromptGuide(): JSX.Element { + const { t } = useTranslation('protocol_generator') + + return ( + + + {t('what_typeof_protocol')} + + + + + {t('make_sure_your_prompt')} + + +
    +
  • + {t('metadata')} + +
  • + {t('application')} +
  • +
  • + {t('robot')} +
  • +
  • + {t('api')} +
  • + + +
  • + {t('ot2_pipettes')} +
  • +
  • + {t('modules')} +
  • +
  • + {t('well_allocations')} +
  • +
  • + , + span: , + }} + /> +
  • +
  • + {t('commands')} +
  • +
+
+
+ , + span: , + }} + /> +
+ ) +} + +const HEADER_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize28}; + line-height: ${TYPOGRAPHY.lineHeight36}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const BODY_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; +` +const StyledUl = styled.ul` + padding-left: ${SPACING.spacing16}; + list-style-type: disc; +` + +const ExternalLink = styled(Link)` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + color: ${COLORS.black90}; + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; +` diff --git a/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx new file mode 100644 index 000000000000..1c1d30b75486 --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/SidePanel.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../i18n' +import { SidePanel as SidePanelComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/molecules/SidePanel', + component: SidePanelComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const SidePanel: Story = {} diff --git a/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx new file mode 100644 index 000000000000..56cb50f73fc9 --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/__tests__/SidePanel.test.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' + +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' + +import { SidePanel } from '../index' + +const LOGO_FILE_NAME = + '/opentrons-ai-client/src/assets/images/opentrons_logo.svg' + +const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/' + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('SidePanel', () => { + it('should render logo and text', () => { + render() + const image = screen.getByRole('img') + expect(image.getAttribute('src')).toEqual(LOGO_FILE_NAME) + screen.getByText( + 'Use natural language to generate protocols with OpentronsAI powered by OpenAI' + ) + screen.getByText( + 'Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.' + ) + screen.getByText('Stuck? Try these example prompts to get started.') + screen.getByText('Got feedback? We love to hear it.') + const link = screen.getByRole('link', { + name: 'Share your thoughts here', + }) + expect(link).toHaveAttribute('href', FEEDBACK_FORM_LINK) + }) + + it('should render buttons', () => { + render() + screen.getByRole('button', { name: 'PCR' }) + screen.getByRole('button', { name: 'PCR (Flex)' }) + screen.getByRole('button', { name: 'Reagent Transfer' }) + screen.getByRole('button', { name: 'Reagent Transfer (Flex)' }) + }) + it.todo('should call a mock function when clicking a button') +}) diff --git a/opentrons-ai-client/src/molecules/SidePanel/index.tsx b/opentrons-ai-client/src/molecules/SidePanel/index.tsx new file mode 100644 index 000000000000..a53927c0293c --- /dev/null +++ b/opentrons-ai-client/src/molecules/SidePanel/index.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import styled, { css } from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Link, + PrimaryButton, + SPACING, + StyledText, + TYPOGRAPHY, + WRAP, +} from '@opentrons/components' +import LOGO_PATH from '../../assets/images/opentrons_logo.svg' + +const IMAGE_ALT = 'Opentrons logo' +const FEEDBACK_FORM_LINK = 'https://opentrons-ai-beta.paperform.co/' +export function SidePanel(): JSX.Element { + const { t } = useTranslation('protocol_generator') + return ( + + {/* logo */} + + {IMAGE_ALT} + + + {/* body text */} + + + {t('side_panel_header')} + + {t('side_panel_body')} + + + {/* buttons */} + + + {t('try_example_prompts')} + + + + {/* ToDo(kk:04/11/2024) add a button component */} + {t('reagent_transfer')} + {t('reagent_transfer_flex')} + {t('prc')} + {t('prc_flex')} + + + + + {t('got_feedback')} + + + {t('share_your_thoughts')} + + + + ) +} + +const HEADER_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize32}; + line-height: ${TYPOGRAPHY.lineHeight42}; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + color: ${COLORS.white}; +` +const BODY_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + color: ${COLORS.white}; +` +const BUTTON_GUIDE_TEXT_STYLE = css` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + color: ${COLORS.white}; +` + +const PromptButton = styled(PrimaryButton)` + border-radius: ${BORDERS.borderRadiusFull}; + white-space: nowrap; +` + +const FeedbackLink = styled(Link)` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightBold}; + color: ${COLORS.white}; + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; +` diff --git a/opentrons-ai-client/src/molecules/index.ts b/opentrons-ai-client/src/molecules/index.ts new file mode 100644 index 000000000000..80fcd68f91a0 --- /dev/null +++ b/opentrons-ai-client/src/molecules/index.ts @@ -0,0 +1 @@ +export * from './SidePanel' diff --git a/opentrons-ai-client/src/organisms/ChatContainer/ChatContainer.stories.tsx b/opentrons-ai-client/src/organisms/ChatContainer/ChatContainer.stories.tsx new file mode 100644 index 000000000000..de3ba5843029 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ChatContainer/ChatContainer.stories.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { I18nextProvider } from 'react-i18next' +import { i18n } from '../../i18n' +import { ChatContainer as ChatContainerComponent } from './index' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + title: 'AI/organisms/ChatContainer', + component: ChatContainerComponent, + decorators: [ + Story => ( + + + + ), + ], +} +export default meta +type Story = StoryObj +export const ChatContainer: Story = {} diff --git a/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx new file mode 100644 index 000000000000..26eb7b0a2b52 --- /dev/null +++ b/opentrons-ai-client/src/organisms/ChatContainer/__tests__/ChatContainer.test.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../__testing-utils__' +import { i18n } from '../../../i18n' +import { PromptGuide } from '../../../molecules/PromptGuide' +import { ChatContainer } from '../index' + +vi.mock('../../../molecules/PromptGuide') + +const render = (): ReturnType => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ChatContainer', () => { + beforeEach(() => { + vi.mocked(PromptGuide).mockReturnValue(
mock PromptGuide
) + }) + it('should render prompt guide and text', () => { + render() + screen.getByText('OpentronsAI') + screen.getByText('mock PromptGuide') + }) + + // ToDo (kk:04/16/2024) Add more test cases +}) diff --git a/opentrons-ai-client/src/organisms/ChatContainer/index.tsx b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx new file mode 100644 index 000000000000..2a6542c8e68b --- /dev/null +++ b/opentrons-ai-client/src/organisms/ChatContainer/index.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + COLORS, + DIRECTION_COLUMN, + FLEX_MAX_CONTENT, + Flex, + SPACING, + StyledText, +} from '@opentrons/components' +import { PromptGuide } from '../../molecules/PromptGuide' + +export function ChatContainer(): JSX.Element { + const { t } = useTranslation('protocol_generator') + const isDummyInitial = true + return ( + + {/* This will be updated when input textbox and function are implemented */} + {isDummyInitial ? ( + + {t('opentronsai')} + + + ) : null} + + ) +} diff --git a/opentrons-ai-client/tsconfig-data.json b/opentrons-ai-client/tsconfig-data.json new file mode 100644 index 000000000000..79a9673faa9a --- /dev/null +++ b/opentrons-ai-client/tsconfig-data.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig-base.json", + "references": [], + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": false, + "rootDir": ".", + "outDir": "lib" + }, + "include": ["src/**/*.json", "fixtures/**/*.json", "vite.config.ts"], + "exclude": ["**/*.ts", "**/*.tsx"] +} diff --git a/opentrons-ai-client/tsconfig.json b/opentrons-ai-client/tsconfig.json new file mode 100644 index 000000000000..b3c6dc275a88 --- /dev/null +++ b/opentrons-ai-client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + { + "path": "./tsconfig-data.json" + }, + { + "path": "../components" + } + ], + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["typings", "src"] +} diff --git a/opentrons-ai-client/typings/images.d.ts b/opentrons-ai-client/typings/images.d.ts new file mode 100644 index 000000000000..9dcd2f687923 --- /dev/null +++ b/opentrons-ai-client/typings/images.d.ts @@ -0,0 +1,15 @@ +declare module '*.png' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} +declare module '*.svg' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} +declare module '*.webm' { + const image: string + // eslint-disable-next-line import/no-default-export + export default image +} diff --git a/opentrons-ai-client/typings/styled-components.d.ts b/opentrons-ai-client/typings/styled-components.d.ts new file mode 100644 index 000000000000..5d6296f94be1 --- /dev/null +++ b/opentrons-ai-client/typings/styled-components.d.ts @@ -0,0 +1 @@ +import 'styled-components/cssprop' diff --git a/opentrons-ai-client/vite.config.ts b/opentrons-ai-client/vite.config.ts new file mode 100644 index 000000000000..ee557f68d628 --- /dev/null +++ b/opentrons-ai-client/vite.config.ts @@ -0,0 +1,43 @@ +import path from 'path' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', + }, + plugins: [ + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), + ], + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [], + }, + }, + define: { + 'process.env': process.env, + global: 'globalThis', + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + }, + }, +}) diff --git a/opentrons-ai-server/Makefile b/opentrons-ai-server/Makefile new file mode 100644 index 000000000000..9de2141f6a0b --- /dev/null +++ b/opentrons-ai-server/Makefile @@ -0,0 +1,2 @@ +# opentrons ai server makefile +# TBD \ No newline at end of file diff --git a/opentrons-ai-server/README.md b/opentrons-ai-server/README.md new file mode 100644 index 000000000000..e00cdc1af3d6 --- /dev/null +++ b/opentrons-ai-server/README.md @@ -0,0 +1,39 @@ +# Opentrons AI Backend + +## Overview + +The Opentrons AI application's server. + +## Developing + +To get started: clone the `Opentrons/opentrons` repository, set up your computer for development as specified in the [contributing guide][contributing-guide-setup], and then: + +```shell +# change into the cloned directory +cd opentrons +# prerequisite: install dependencies as specified in project setup +make setup +# launch the dev server +make -C opentrons-ai-server dev +``` + +## Stack and structure + +The UI stack is built using: + +- [OpenAI Python API library][] + +Some important directories: + +- `opentrons-ai-client` — Opentrons AI application's client-side + +## Testing + +TBD + +## Building + +TBD + +[pytest]: https://docs.pytest.org/en/ +[openai python api library]: https://pypi.org/project/openai/ diff --git a/package.json b/package.json index af9335f4be6d..007e9328c012 100755 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "shx": "^0.3.3", "simple-git": "^3.15.1", "storybook": "^7.6.16", - "storybook-addon-pseudo-states": "^1.15.5", + "storybook-addon-pseudo-states": "2.0.0", "style-loader": "^1.1.3", "stylelint": "^11.0.0", "stylelint-config-standard": "^19.0.0", diff --git a/performance-metrics/.flake8 b/performance-metrics/.flake8 new file mode 100644 index 000000000000..4aa1c02d7aa7 --- /dev/null +++ b/performance-metrics/.flake8 @@ -0,0 +1,25 @@ +[flake8] + +# max cyclomatic complexity +max-complexity = 9 + +extend-ignore = + # defer formatting concerns to black + # E203: space around `:` operator + # E501: maximum line length + E203, + E501, + # do not require type annotations for self nor cls + ANN101, + ANN102 + # do not require docstring for __init__, put them on the class + D107, + +# configure flake8-docstrings +# https://pypi.org/project/flake8-docstrings/ +docstring-convention = google + +noqa-require-code = true + +per-file-ignores = + setup.py:ANN,D \ No newline at end of file diff --git a/performance-metrics/.gitignore b/performance-metrics/.gitignore new file mode 100644 index 000000000000..8fb3d9a4ea59 --- /dev/null +++ b/performance-metrics/.gitignore @@ -0,0 +1 @@ +.ruff_cache/ \ No newline at end of file diff --git a/performance-metrics/Makefile b/performance-metrics/Makefile new file mode 100644 index 000000000000..fd4dd421ad26 --- /dev/null +++ b/performance-metrics/Makefile @@ -0,0 +1,32 @@ +include ../scripts/python.mk + +.PHONY: lint +lint: + $(python) -m black --check . + $(python) -m flake8 . + $(python) -m mypy . + +.PHONY: format +format: + $(python) -m black . + +.PHONY: setup +setup: + $(pipenv) sync --dev + +.PHONY: teardown +teardown: + $(pipenv) --rm + +.PHONY: clean +clean: + rm -rf build dist *.egg-info .mypy_cache .pytest_cache src/performance_metrics.egg-info + +.PHONY: wheel +wheel: + $(python) setup.py $(wheel_opts) bdist_wheel + rm -rf build + +.PHONY: test +test: + $(pytest) tests \ No newline at end of file diff --git a/performance-metrics/Pipfile b/performance-metrics/Pipfile new file mode 100644 index 000000000000..a71db703e335 --- /dev/null +++ b/performance-metrics/Pipfile @@ -0,0 +1,21 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +opentrons-shared-data = {file = "../shared-data/python", editable = true} +performance-metrics = {file = ".", editable = true} + +[dev-packages] +pytest = "==7.4.4" +mypy = "==1.8.0" +flake8 = "==7.0.0" +flake8-annotations = "~=3.0.1" +flake8-docstrings = "~=1.7.0" +flake8-noqa = "~=1.4.0" +black = "==22.3.0" +pytest-asyncio = "~=0.23.0" + +[requires] +python_version = "3.10" diff --git a/performance-metrics/Pipfile.lock b/performance-metrics/Pipfile.lock new file mode 100644 index 000000000000..5c836231b7e8 --- /dev/null +++ b/performance-metrics/Pipfile.lock @@ -0,0 +1,380 @@ +{ + "_meta": { + "hash": { + "sha256": "d811fa2b7dca8a5be8b2dba79ab7200243b2e10fb65f9ee221623f2710b24372" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "opentrons-shared-data": { + "editable": true, + "file": "../shared-data/python", + "markers": "python_version >= '3.8'" + }, + "performance-metrics": { + "editable": true, + "file": "." + }, + "pydantic": { + "hashes": [ + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.15" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.2'", + "version": "==22.3.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", + "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.0" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-annotations": { + "hashes": [ + "sha256:af78e3216ad800d7e144745ece6df706c81b3255290cbf870e54879d495e8ade", + "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==3.0.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", + "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.7.0" + }, + "flake8-noqa": { + "hashes": [ + "sha256:4465e16a19be433980f6f563d05540e2e54797eb11facb9feb50fed60624dc45", + "sha256:771765ab27d1efd157528379acd15131147f9ae578a72d17fb432ca197881243" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.4.0" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02", + "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835", + "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e", + "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a", + "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410", + "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.8.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.4" + }, + "pytest-asyncio": { + "hashes": [ + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.23.6" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version >= '3.8'", + "version": "==4.11.0" + } + } +} diff --git a/performance-metrics/README.md b/performance-metrics/README.md new file mode 100644 index 000000000000..7fb20445e366 --- /dev/null +++ b/performance-metrics/README.md @@ -0,0 +1,3 @@ +# Performance Metrics + +Project to gather various performance metrics for the Opentrons Flex. diff --git a/performance-metrics/mypy.ini b/performance-metrics/mypy.ini new file mode 100644 index 000000000000..b94476cbcaa8 --- /dev/null +++ b/performance-metrics/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +show_error_codes = True +warn_unused_configs = True +strict = True +exclude = setup.py \ No newline at end of file diff --git a/performance-metrics/pytest.ini b/performance-metrics/pytest.ini new file mode 100644 index 000000000000..49f044127468 --- /dev/null +++ b/performance-metrics/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --color=yes --strict-markers +asyncio_mode = auto diff --git a/performance-metrics/setup.py b/performance-metrics/setup.py new file mode 100755 index 000000000000..eced9a55ab90 --- /dev/null +++ b/performance-metrics/setup.py @@ -0,0 +1,91 @@ +# Inspired by: +# https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ +import sys +import codecs +import os +import os.path +from setuptools import setup, find_packages + +# make stdout blocking since Travis sets it to nonblocking +if os.name == "posix": + import fcntl + + flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) + fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + +HERE = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(HERE, "..", "scripts")) + +from python_build_utils import normalize_version # noqa: E402 + + +def get_version(): + buildno = os.getenv("BUILD_NUMBER") + project = os.getenv("OPENTRONS_PROJECT", "robot-stack") + git_dir = os.getenv("OPENTRONS_GIT_DIR", None) + if buildno: + normalize_opts = {"extra_tag": buildno} + else: + normalize_opts = {} + return normalize_version( + "performance-metrics", project, git_dir=git_dir, **normalize_opts + ) + + +VERSION = get_version() + +DISTNAME = "performance_metrics" +LICENSE = "Apache 2.0" +AUTHOR = "Opentrons" +EMAIL = "engineering@opentrons.com" +URL = "https://github.com/Opentrons/opentrons" +DOWNLOAD_URL = "" +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] +DESCRIPTION = "Library for working with performance metrics on the Opentrons robots" +PACKAGES = find_packages(where="src", exclude=["tests.*", "tests"]) +INSTALL_REQUIRES = [ + f"opentrons-shared-data=={VERSION}", +] + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +if __name__ == "__main__": + setup( + python_requires="~=3.10", + name=DISTNAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + keywords=KEYWORDS, + long_description=__doc__, + packages=PACKAGES, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + package_dir={"": "src"}, + package_data={"performance-metrics": ["py.typed"]}, + ) diff --git a/performance-metrics/src/performance_metrics/__init__.py b/performance-metrics/src/performance_metrics/__init__.py new file mode 100644 index 000000000000..b5f2e760c197 --- /dev/null +++ b/performance-metrics/src/performance_metrics/__init__.py @@ -0,0 +1,5 @@ +"""Opentrons performance metrics library.""" + +from .robot_context_tracker import RobotContextTracker + +__all__ = ["RobotContextTracker"] diff --git a/performance-metrics/src/performance_metrics/datashapes.py b/performance-metrics/src/performance_metrics/datashapes.py new file mode 100644 index 000000000000..7743ed1723d1 --- /dev/null +++ b/performance-metrics/src/performance_metrics/datashapes.py @@ -0,0 +1,35 @@ +"""Defines data classes and enums used in the performance metrics module.""" + +import dataclasses +from typing import Tuple +from opentrons_shared_data.performance.dev_types import RobotContextState + + +@dataclasses.dataclass(frozen=True) +class RawContextData: + """Represents raw duration data with context state information. + + Attributes: + - function_start_time (int): The start time of the function. + - duration_measurement_start_time (int): The start time for duration measurement. + - duration_measurement_end_time (int): The end time for duration measurement. + - state (RobotContextStates): The current state of the context. + """ + + func_start: int + duration_start: int + duration_end: int + state: RobotContextState + + @classmethod + def headers(self) -> Tuple[str, str, str]: + """Returns the headers for the raw context data.""" + return ("state_id", "function_start_time", "duration") + + def csv_row(self) -> Tuple[int, int, int]: + """Returns the raw context data as a string.""" + return ( + self.state.state_id, + self.func_start, + self.duration_end - self.duration_start, + ) diff --git a/api/tests/opentrons/commands/__init__.py b/performance-metrics/src/performance_metrics/py.typed similarity index 100% rename from api/tests/opentrons/commands/__init__.py rename to performance-metrics/src/performance_metrics/py.typed diff --git a/performance-metrics/src/performance_metrics/robot_context_tracker.py b/performance-metrics/src/performance_metrics/robot_context_tracker.py new file mode 100644 index 000000000000..99dc502c9ad2 --- /dev/null +++ b/performance-metrics/src/performance_metrics/robot_context_tracker.py @@ -0,0 +1,103 @@ +"""Module for tracking robot context and execution duration for different operations.""" + +import csv +from pathlib import Path +import platform + +from functools import wraps, partial +from time import perf_counter_ns +import os +from typing import Callable, TypeVar, cast + + +from typing_extensions import ParamSpec +from collections import deque +from performance_metrics.datashapes import ( + RawContextData, +) +from opentrons_shared_data.performance.dev_types import ( + RobotContextState, + SupportsTracking, +) + +P = ParamSpec("P") +R = TypeVar("R") + + +def _get_timing_function() -> Callable[[], int]: + """Returns a timing function for the current platform.""" + time_function: Callable[[], int] + if platform.system() == "Linux": + from time import clock_gettime_ns, CLOCK_REALTIME + + time_function = cast( + Callable[[], int], partial(clock_gettime_ns, CLOCK_REALTIME) + ) + else: + from time import time_ns + + time_function = time_ns + + return time_function + + +timing_function = _get_timing_function() + + +class RobotContextTracker(SupportsTracking): + """Tracks and stores robot context and execution duration for different operations.""" + + FILE_NAME = "context_data.csv" + + def __init__(self, storage_location: Path, should_track: bool = False) -> None: + """Initializes the RobotContextTracker with an empty storage list.""" + self._storage: deque[RawContextData] = deque() + self._storage_file_path = storage_location / self.FILE_NAME + self._should_track = should_track + + def track(self, state: RobotContextState) -> Callable: # type: ignore + """Decorator factory for tracking the execution duration and state of robot operations. + + Args: + state: The state to track for the decorated function. + + Returns: + Callable: A decorator that wraps a function to track its execution duration and state. + """ + + def inner_decorator(func: Callable[P, R]) -> Callable[P, R]: + if not self._should_track: + return func + + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + function_start_time = timing_function() + duration_start_time = perf_counter_ns() + try: + result = func(*args, **kwargs) + finally: + duration_end_time = perf_counter_ns() + self._storage.append( + RawContextData( + function_start_time, + duration_start_time, + duration_end_time, + state, + ) + ) + return result + + return wrapper + + return inner_decorator + + def store(self) -> None: + """Returns the stored context data and clears the storage list.""" + stored_data = self._storage.copy() + self._storage.clear() + rows_to_write = [context_data.csv_row() for context_data in stored_data] + os.makedirs(self._storage_file_path.parent, exist_ok=True) + with open(self._storage_file_path, "a") as storage_file: + writer = csv.writer(storage_file) + writer.writerow(RawContextData.headers()) + writer.writerows(rows_to_write) diff --git a/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py new file mode 100644 index 000000000000..2c112410063a --- /dev/null +++ b/performance-metrics/tests/performance_metrics/test_robot_context_tracker.py @@ -0,0 +1,305 @@ +"""Tests for the RobotContextTracker class in performance_metrics.robot_context_tracker.""" + +import asyncio +from pathlib import Path +import pytest +from performance_metrics.robot_context_tracker import RobotContextTracker +from opentrons_shared_data.performance.dev_types import RobotContextState +from time import sleep, time_ns +from unittest.mock import patch + +# Corrected times in seconds +STARTING_TIME = 0.001 +CALIBRATING_TIME = 0.002 +ANALYZING_TIME = 0.003 +RUNNING_TIME = 0.004 +SHUTTING_DOWN_TIME = 0.005 + + +@pytest.fixture +def robot_context_tracker(tmp_path: Path) -> RobotContextTracker: + """Fixture to provide a fresh instance of RobotContextTracker for each test.""" + return RobotContextTracker(storage_location=tmp_path, should_track=True) + + +def test_robot_context_tracker(robot_context_tracker: RobotContextTracker) -> None: + """Tests the tracking of various robot context states through RobotContextTracker.""" + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + def analyzing_protocol() -> None: + sleep(ANALYZING_TIME) + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def running_protocol() -> None: + sleep(RUNNING_TIME) + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + def shutting_down_robot() -> None: + sleep(SHUTTING_DOWN_TIME) + + # Ensure storage is initially empty + assert ( + len(robot_context_tracker._storage) == 0 + ), "Storage should be initially empty." + + starting_robot() + calibrating_robot() + analyzing_protocol() + running_protocol() + shutting_down_robot() + + # Verify that all states were tracked + assert len(robot_context_tracker._storage) == 5, "All states should be tracked." + + # Validate the sequence and accuracy of tracked states + expected_states = [ + RobotContextState.STARTING_UP, + RobotContextState.CALIBRATING, + RobotContextState.ANALYZING_PROTOCOL, + RobotContextState.RUNNING_PROTOCOL, + RobotContextState.SHUTTING_DOWN, + ] + for i, state in enumerate(expected_states): + assert ( + RobotContextState.from_id(robot_context_tracker._storage[i].state.state_id) + == state + ), f"State at index {i} should be {state}." + + +def test_multiple_operations_single_state( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking multiple operations within a single robot context state.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def first_operation() -> None: + sleep(RUNNING_TIME) + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def second_operation() -> None: + sleep(RUNNING_TIME) + + first_operation() + second_operation() + + assert ( + len(robot_context_tracker._storage) == 2 + ), "Both operations should be tracked." + assert ( + robot_context_tracker._storage[0].state + == robot_context_tracker._storage[1].state + == RobotContextState.RUNNING_PROTOCOL + ), "Both operations should have the same state." + + +def test_exception_handling_in_tracked_function( + robot_context_tracker: RobotContextTracker, +) -> None: + """Ensures exceptions in tracked operations are handled correctly.""" + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + def error_prone_operation() -> None: + sleep(SHUTTING_DOWN_TIME) + raise RuntimeError("Simulated operation failure") + + with pytest.raises(RuntimeError): + error_prone_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Failed operation should still be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.SHUTTING_DOWN + ), "State should be correctly logged despite the exception." + + +@pytest.mark.asyncio +async def test_async_operation_tracking( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking of an asynchronous operation.""" + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + async def async_analyzing_operation() -> None: + await asyncio.sleep(ANALYZING_TIME) + + await async_analyzing_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Async operation should be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.ANALYZING_PROTOCOL + ), "State should be ANALYZING_PROTOCOL." + + +def test_sync_operation_timing_accuracy( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests the timing accuracy of a synchronous operation tracking.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + def running_operation() -> None: + sleep(RUNNING_TIME) + + running_operation() + + duration_data = robot_context_tracker._storage[0] + measured_duration = duration_data.duration_end - duration_data.duration_start + assert ( + abs(measured_duration - RUNNING_TIME * 1e9) < 1e7 + ), "Measured duration for sync operation should closely match the expected duration." + + +@pytest.mark.asyncio +async def test_async_operation_timing_accuracy( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests the timing accuracy of an async operation tracking.""" + + @robot_context_tracker.track(state=RobotContextState.RUNNING_PROTOCOL) + async def async_running_operation() -> None: + await asyncio.sleep(RUNNING_TIME) + + await async_running_operation() + + duration_data = robot_context_tracker._storage[0] + measured_duration = duration_data.duration_end - duration_data.duration_start + assert ( + abs(measured_duration - RUNNING_TIME * 1e9) < 1e7 + ), "Measured duration for async operation should closely match the expected duration." + + +@pytest.mark.asyncio +async def test_exception_in_async_operation( + robot_context_tracker: RobotContextTracker, +) -> None: + """Ensures exceptions in tracked async operations are correctly handled.""" + + @robot_context_tracker.track(state=RobotContextState.SHUTTING_DOWN) + async def async_error_prone_operation() -> None: + await asyncio.sleep(SHUTTING_DOWN_TIME) + raise RuntimeError("Simulated async operation failure") + + with pytest.raises(RuntimeError): + await async_error_prone_operation() + + assert ( + len(robot_context_tracker._storage) == 1 + ), "Failed async operation should still be tracked." + assert ( + robot_context_tracker._storage[0].state == RobotContextState.SHUTTING_DOWN + ), "State should be SHUTTING_DOWN despite the exception." + + +@pytest.mark.asyncio +async def test_concurrent_async_operations( + robot_context_tracker: RobotContextTracker, +) -> None: + """Tests tracking of concurrent async operations.""" + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + async def first_async_calibrating() -> None: + await asyncio.sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + async def second_async_calibrating() -> None: + await asyncio.sleep(CALIBRATING_TIME) + + await asyncio.gather(first_async_calibrating(), second_async_calibrating()) + + assert ( + len(robot_context_tracker._storage) == 2 + ), "Both concurrent async operations should be tracked." + assert all( + data.state == RobotContextState.CALIBRATING + for data in robot_context_tracker._storage + ), "All tracked operations should be in CALIBRATING state." + + +def test_no_tracking(tmp_path: Path) -> None: + """Tests that operations are not tracked when tracking is disabled.""" + robot_context_tracker = RobotContextTracker(tmp_path, should_track=False) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def operation_without_tracking() -> None: + sleep(STARTING_TIME) + + operation_without_tracking() + + assert ( + len(robot_context_tracker._storage) == 0 + ), "Operation should not be tracked when tracking is disabled." + + +async def test_storing_to_file(tmp_path: Path) -> None: + """Tests storing the tracked data to a file.""" + robot_context_tracker = RobotContextTracker(tmp_path, should_track=True) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + @robot_context_tracker.track(state=RobotContextState.ANALYZING_PROTOCOL) + def analyzing_protocol() -> None: + sleep(ANALYZING_TIME) + + starting_robot() + calibrating_robot() + analyzing_protocol() + + robot_context_tracker.store() + + with open(robot_context_tracker._storage_file_path, "r") as file: + lines = file.readlines() + assert ( + len(lines) == 4 + ), "All stored data + header should be written to the file." + + +@patch( + "performance_metrics.robot_context_tracker._get_timing_function", + return_value=time_ns, +) +def test_using_non_linux_time_functions(tmp_path: Path) -> None: + """Tests tracking operations using non-Linux time functions.""" + file_path = tmp_path / "test_file.csv" + robot_context_tracker = RobotContextTracker(file_path, should_track=True) + + @robot_context_tracker.track(state=RobotContextState.STARTING_UP) + def starting_robot() -> None: + sleep(STARTING_TIME) + + @robot_context_tracker.track(state=RobotContextState.CALIBRATING) + def calibrating_robot() -> None: + sleep(CALIBRATING_TIME) + + starting_robot() + calibrating_robot() + + storage = robot_context_tracker._storage + assert all( + data.func_start > 0 for data in storage + ), "All function start times should be greater than 0." + assert all( + data.duration_start > 0 for data in storage + ), "All duration start times should be greater than 0." + assert all( + data.duration_end > 0 for data in storage + ), "All duration end times should be greater than 0." + assert all( + data.duration_end > data.duration_start for data in storage + ), "Duration end times should be greater than duration start times." + assert len(storage) == 2, "Both operations should be tracked." diff --git a/protocol-designer/cypress/e2e/migrations.cy.js b/protocol-designer/cypress/e2e/migrations.cy.js index 6c1d01a0ee7b..4339f40be5f1 100644 --- a/protocol-designer/cypress/e2e/migrations.cy.js +++ b/protocol-designer/cypress/e2e/migrations.cy.js @@ -127,7 +127,7 @@ describe('Protocol fixtures migrate and match snapshots', () => { cy.get('div') .contains( - 'This protocol can only run on app and robot server version 7.1 or higher' + 'This protocol can only run on app and robot server version 7.3.0 or higher' ) .should('exist') cy.get('button').contains('continue', { matchCase: false }).click() diff --git a/protocol-designer/cypress/e2e/mixSettings.cy.js b/protocol-designer/cypress/e2e/mixSettings.cy.js index a3e32cfdd689..60885a9b5c36 100644 --- a/protocol-designer/cypress/e2e/mixSettings.cy.js +++ b/protocol-designer/cypress/e2e/mixSettings.cy.js @@ -60,7 +60,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="aspirate_flowRate"]').should('be.disabled') // TipPosition Aspirate should be disabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.enabled') // Dispense Flowrate disbled cy.get('input[name="dispense_flowRate"]').should('be.disabled') @@ -92,7 +92,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('input[name="dispense_flowRate"]').should('be.enabled') // TipPosition Aspirate should be enabled - cy.get('[id=TipPositionField_mix_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.disabled') // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/cypress/e2e/transferSettings.cy.js b/protocol-designer/cypress/e2e/transferSettings.cy.js index 2bb870bb3645..c5e2f851afc2 100644 --- a/protocol-designer/cypress/e2e/transferSettings.cy.js +++ b/protocol-designer/cypress/e2e/transferSettings.cy.js @@ -55,7 +55,7 @@ describe('Advanced Settings for Transfer Form', () => { enterBatchEdit() - // Different Pipette disbales aspirate and dispense Flowrate and Mix settings + // Different Pipette disables aspirate and dispense Flowrate and Mix settings // step 6 has different pipette than step 1 cy.get('[data-test="StepItem_6"]').click(batchEditClickOptions) @@ -70,10 +70,14 @@ describe('Advanced Settings for Transfer Form', () => { cy.get('input[name="aspirate_mix_checkbox"]').should('be.disabled') // TipPosition Aspirate and Dispense should be disabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.disabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.disabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.enabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.enabled' + ) - // Dispense Flowrate and mix diabled + // Dispense Flowrate and mix disabled cy.get('input[name="dispense_flowRate"]').should('be.disabled') cy.get('input[name="dispense_mix_checkbox"]').should('be.disabled') @@ -110,8 +114,12 @@ describe('Advanced Settings for Transfer Form', () => { .should('be.empty') // TipPosition Aspirate and Dispense should be enabled - cy.get('[id=TipPositionField_aspirate_mmFromBottom]').should('be.enabled') - cy.get('[id=TipPositionField_dispense_mmFromBottom]').should('be.enabled') + cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( + 'not.be.disabled' + ) + cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( + 'not.be.disabled' + ) // Delay in aspirate and Dispense settings is enabled cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 9bc7b9e44ed7..e448368f9328 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v3 commands", "created": 1585930833548, - "lastModified": 1709303240330, + "lastModified": 1711742442671, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -101,6 +101,7 @@ "dispense_touchTip_mmFromBottom": 40, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "preWetTip": false, @@ -116,6 +117,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -153,6 +158,7 @@ "labware": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "mix_mmFromBottom": 0.5, @@ -170,6 +176,8 @@ "dropTip_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin", "nozzles": null, "tipRack": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "a4cee9a0-75dc-11ea-b42f-4b64e50f43e5", "stepType": "mix", "stepName": "mix", @@ -2518,7 +2526,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "db2d2973-9059-41a8-a6f7-3b70b747cb2d", + "key": "d371b7e2-71a8-4a60-90bc-7e865d9881b9", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2527,7 +2535,7 @@ } }, { - "key": "d9bb5f59-77e8-4794-af52-5ac18181a1c9", + "key": "424963b7-59f8-434a-bedc-9597e7b72c9f", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2539,7 +2547,7 @@ } }, { - "key": "e375681d-7284-4f0c-9921-d16e4ce0649e", + "key": "05ef86f7-dec0-4134-a15d-5e38ef81cf8e", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2551,7 +2559,7 @@ } }, { - "key": "9455cd08-4d3a-45e4-8614-7485193e824e", + "key": "ddefc5ef-b69a-4172-921b-959ba5e8d8d2", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2564,7 +2572,7 @@ }, { "commandType": "loadLiquid", - "key": "27c67940-a745-41b9-b4d8-01a8dba8b4e9", + "key": "2a2084d5-67d8-4806-b919-5962a6258c1f", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2590,12 +2598,12 @@ }, { "commandType": "waitForDuration", - "key": "b7c8d36b-c9d6-4fa3-a696-da35a3cc5981", + "key": "c1a1eff4-7ef7-46be-aee7-ebca5924ace8", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "d97988e7-e386-4965-a915-f4776a0d7720", + "key": "63ca0ab5-4cb6-4531-b912-1ba22e1b1a03", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2604,67 +2612,82 @@ }, { "commandType": "aspirate", - "key": "a60e86c1-8bf0-477f-8748-24ce798eb1de", + "key": "5ead7532-0eb2-4ad9-b704-856422fc9408", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "61afaad0-0566-4435-b03a-94498d2fc2aa", + "key": "3838f7d1-3450-49cc-a222-c8113eecf108", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "4ccf6427-d404-4b4b-9974-935a6676d8d2", + "key": "25697ae7-169d-447a-906c-4e7f02950fe9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "bcdd9d53-ff68-4264-bd61-e11422149144", + "key": "49a139f4-87ba-421d-9ef4-4ebe13beb987", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "aspirate", - "key": "ce51dbac-b2ed-4edd-9657-33c106288844", + "key": "4e96faa5-c669-4b60-b15c-9d2f01c9c3fe", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 100, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "a1993756-c789-4804-8ff0-f3f9577d68f4", + "key": "8eff88a1-fec9-46d7-b292-f6ce378e5ad9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2674,19 +2697,22 @@ }, { "commandType": "dispense", - "key": "270e73e1-719a-4338-9a8d-7ef8cdab558e", + "key": "c95e323c-be69-4460-8acf-d1d4b74384bd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "45fd9725-1bef-4333-bce3-e4e81fc94fd4", + "key": "0da25745-5e25-4138-b67c-dfc4c89c8949", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2696,19 +2722,22 @@ }, { "commandType": "dispense", - "key": "ede9c8e0-ced4-4a60-841e-c28476d28ab8", + "key": "28eeb3d1-6e83-4414-8c0d-e8761ca2f75a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 40, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "touchTip", - "key": "d7fb1df9-ee04-4a6d-98e0-1ded591260bc", + "key": "8cd5d90d-df0b-4c3b-8cb3-cea6f1849fef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", @@ -2718,7 +2747,7 @@ }, { "commandType": "moveToAddressableArea", - "key": "35f1f9b9-78f2-4a1a-9b7b-3c488881db2b", + "key": "35643d1f-ae0b-4a90-9de4-c9eb3c9b775e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2727,7 +2756,7 @@ }, { "commandType": "blowOutInPlace", - "key": "7dbec34d-5da6-41aa-9ff9-9368efa23407", + "key": "d540a57a-6968-44a0-8645-b221a9b7bfd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 46.43 @@ -2735,7 +2764,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c10381b-bee1-4908-94c5-11d76a966a12", + "key": "c721cfd7-fef8-4fcb-9d6f-1d78f2317729", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2745,17 +2774,17 @@ }, { "commandType": "dropTipInPlace", - "key": "fb84a594-b8b9-4950-81f0-cc2be260346e", + "key": "a18788f3-cd5f-4470-8831-455d14883d1c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "waitForResume", - "key": "340df2fa-adf0-4b43-90d7-2e1d8f09ba71", + "key": "a54eb58b-ce5c-4a59-ba85-ed75438146a7", "params": { "message": "Wait until user intervention" } }, { "commandType": "pickUpTip", - "key": "9ff49b09-2860-4955-bd6f-a68ab3797208", + "key": "c1bddcd0-d5cf-4d7c-b830-a5b27a5a71cb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2764,79 +2793,97 @@ }, { "commandType": "aspirate", - "key": "249a56b1-e68b-449d-aad1-9a4bd9113a34", + "key": "1660f6c2-9072-4348-b034-cb45712f8cd7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "5503460e-fc28-4ebf-b476-88d2517ec4c5", + "key": "c3683fde-b4e0-4432-ad96-932292f2ebcd", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "75c377d3-a93f-4310-9c06-1ee6e1d2fdb1", + "key": "59251222-f64d-400b-98a6-71f95f24bec7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "c0ad7408-1f69-4092-8de0-524a0c3991e4", + "key": "a370936b-c12f-4039-88d0-97bb262cb80e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "2c04b095-4f0c-4cd7-a1bc-8daee4e05f38", + "key": "8f428646-3bd6-4a90-9674-23d3e3be8a63", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "8ead30e3-b057-4994-b00c-c18a838d86ad", + "key": "445797f5-5799-486a-b0e2-299e2f23ca2a", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "D2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "41dc50be-94fb-49f6-9ac6-9c8948622640", + "key": "d740d713-a3cb-4bdb-81a5-798059db8be7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2845,7 +2892,7 @@ }, { "commandType": "blowOutInPlace", - "key": "4147917f-bca1-4ef4-b055-0610002a3572", + "key": "ba227a58-a0b1-4d83-93f8-4a3566cbedf1", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2853,7 +2900,7 @@ }, { "commandType": "touchTip", - "key": "5692383d-d3c3-4969-9b76-c5dfd265e4c5", + "key": "68b765bb-a232-49ec-b6be-fc6b375b0a15", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2863,7 +2910,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "e389e5ae-8109-4a68-a8e5-58d96f453a85", + "key": "1464952c-cb00-48eb-a9db-8a4367d3ce0b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2873,12 +2920,12 @@ }, { "commandType": "dropTipInPlace", - "key": "82048ca4-6ad6-4de9-ad94-4fc698e3aaff", + "key": "2d96c742-46d0-4efa-8e94-3118e975bdd4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "b868a416-8074-4e21-8483-39b0bbc89ba2", + "key": "75a6817c-7f41-4a8c-a184-5e6e7aad51e9", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2887,79 +2934,97 @@ }, { "commandType": "aspirate", - "key": "677f3413-0fb0-428d-875a-32d3c8971ca1", + "key": "3e1db7e3-a5eb-473c-a98b-1c91e9b70c3d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "6cafa525-b6f6-4ab4-8919-6398ecdcad50", + "key": "d37facff-0753-4d92-9599-93141c97a90f", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "ef66610b-0d69-405b-91e9-9d46ef6f9e49", + "key": "df03e618-352a-44e8-8890-859f53229f10", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "88fbf912-ebaf-4148-9339-2b8fe5d8381d", + "key": "0b93f43f-b456-47fa-b9d7-89086cd9c20b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "aspirate", - "key": "86107fa6-c935-4123-bf58-76643dc888d5", + "key": "310303b6-76e3-4765-bd82-042eac727669", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 40 } }, { "commandType": "dispense", - "key": "2c8d07b1-3f65-4554-99aa-3bc8899a5bd6", + "key": "9881ac40-2932-4197-a03b-77c936651a3b", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 35, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "E2", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 35 } }, { "commandType": "moveToAddressableArea", - "key": "52740457-0f28-44b5-a053-80a7b8be7932", + "key": "f521a11f-1676-4dc2-a022-f5eba1c5d22e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2968,7 +3033,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cff38f29-4334-4fe3-a361-465f2ce46be5", + "key": "daede461-9d74-4259-91e6-ecf7ddaa4897", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "flowRate": 35 @@ -2976,7 +3041,7 @@ }, { "commandType": "touchTip", - "key": "e9c841a0-f8e2-4f07-9eb6-6d03764259a6", + "key": "0cde152c-2aeb-4e86-9745-3732e0074ba7", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2986,7 +3051,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f1dc8237-78ef-4116-88f5-42d426086e63", + "key": "cb24aade-655e-4f6f-83d7-1b60457b56e6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2996,7 +3061,7 @@ }, { "commandType": "dropTipInPlace", - "key": "c7ebd1ef-9d28-43dc-9fdd-6142a1b22c70", + "key": "6970ad16-6e47-4f5c-afba-3704abe0eabb", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } } ], diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index 6a3d3888cba3..f8fec2171afa 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Fixture", "description": "Test all v4 commands", "created": 1585930833548, - "lastModified": 1709303209919, + "lastModified": 1711742493128, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -135,6 +135,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "20", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "preWetTip": false, @@ -150,6 +151,10 @@ "dispense_delay_mmFromBottom": "0.5", "dropTip_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", @@ -2546,7 +2551,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "ee3dbe0a-f7b1-4995-8449-dea339f61737", + "key": "b7185c84-9b15-4b6e-a315-e331249569fa", "commandType": "loadPipette", "params": { "pipetteName": "p300_single_gen2", @@ -2555,7 +2560,7 @@ } }, { - "key": "248415e4-9ae5-4741-9799-9184775c2d31", + "key": "0d1f6599-70d5-4e99-9608-7d249135b5a9", "commandType": "loadModule", "params": { "model": "magneticModuleV2", @@ -2564,7 +2569,7 @@ } }, { - "key": "94f5969a-7e98-47bc-aa0b-eea46b0271a8", + "key": "2ee81ffe-c8fa-4cac-be56-62a902e301f7", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -2573,7 +2578,7 @@ } }, { - "key": "2ee5efc8-5c75-4cc6-8bea-0f258478f0af", + "key": "e1da2e62-ac25-405f-b896-99384ab081d8", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Tip Rack 300 µL", @@ -2585,7 +2590,7 @@ } }, { - "key": "352f2e8e-87e1-4658-a86e-153e5307f35c", + "key": "2895d8a7-239c-4d6b-afc8-69defe261790", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -2599,7 +2604,7 @@ } }, { - "key": "95ee1321-124a-4e78-8b9a-517455c40ab0", + "key": "46b84345-0c06-41f8-860d-1dfafa424e80", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap", @@ -2614,7 +2619,7 @@ }, { "commandType": "loadLiquid", - "key": "44de4f93-8550-465d-b26b-6a2f95d411c1", + "key": "25dd8768-7731-4dee-9f5a-d54b9eb0983c", "params": { "liquidId": "0", "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", @@ -2640,7 +2645,7 @@ }, { "commandType": "magneticModule/engage", - "key": "eb54de80-449c-4287-ae26-5fe7cae3fa3a", + "key": "3471fe25-a3a8-4be0-b6d8-545819c4aea0", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType", "height": 6 @@ -2648,7 +2653,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "a0123190-8242-4c09-bb02-6f78d8c5e493", + "key": "610ae127-200b-48ae-8cbc-7ba4b5ca7b30", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2656,12 +2661,12 @@ }, { "commandType": "waitForDuration", - "key": "6eb18da1-b4ae-4adc-8384-a06b4c21d898", + "key": "94aa4488-7792-49bc-ac3d-6a260bad0f86", "params": { "seconds": 62, "message": "" } }, { "commandType": "pickUpTip", - "key": "ff0fb666-871c-43b8-87d9-9c71fdc0efc9", + "key": "1a838ef5-ea1a-4680-bac0-6eaf473465a4", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2670,31 +2675,37 @@ }, { "commandType": "aspirate", - "key": "398bbf30-90e7-4e50-b630-fb02ddd00160", + "key": "f74c2687-f02c-4034-aa03-9a73c1ee47af", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "0b0ff1c4-0167-4980-b710-794df0799956", + "key": "507c7fff-1193-4c14-a0b1-e4bb9fe9d96e", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "7c8d4e34-5282-4ee8-bcae-36604b949bde", + "key": "5a050ced-d1a9-4031-bf16-ed49cb561e60", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2704,12 +2715,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7ba94010-e87e-448b-8535-70ad404a5f19", + "key": "8083dcbe-8c00-4178-90c0-4d4a921bca9c", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "pickUpTip", - "key": "ceab71fc-ea60-4cbe-8302-7e38a8d27847", + "key": "e6db98b2-7239-4f6b-9e41-02e1dd108ad6", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "labwareId": "0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1", @@ -2718,31 +2729,37 @@ }, { "commandType": "aspirate", - "key": "eda46364-da03-4582-9998-dd91945f08fc", + "key": "47cf3011-68e2-40cd-8563-145e460f93aa", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "dispense", - "key": "42d98996-7605-4d70-b3be-e6a802022a32", + "key": "1f1d966a-9095-4857-9137-36131c91bfd2", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "volume": 30, "labwareId": "21ed8f60-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 46.43 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "97c7e6ee-b6c5-4708-bc85-e5cba1c93a1b", + "key": "ac6074f6-2f28-4012-914b-d3b28eb8453d", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5", "addressableAreaName": "fixedTrash", @@ -2752,12 +2769,12 @@ }, { "commandType": "dropTipInPlace", - "key": "11b30838-4205-4141-9d81-7e2bbde8c7aa", + "key": "074050d3-0c4c-4fc0-8036-a5dc9afe99ef", "params": { "pipetteId": "0b3f2210-75c7-11ea-b42f-4b64e50f43e5" } }, { "commandType": "temperatureModule/waitForTemperature", - "key": "3748a664-b9d8-49fa-9f6b-3ad35eec5c2b", + "key": "89672a34-bd2f-4e2a-bacc-407bb5f563a1", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType", "celsius": 25 @@ -2765,19 +2782,19 @@ }, { "commandType": "magneticModule/disengage", - "key": "a1c763ef-3712-495f-998b-651566f3e759", + "key": "26603c88-f0a7-49b3-a65c-37e9e23ac2ff", "params": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType" } }, { "commandType": "waitForResume", - "key": "f4c1a79c-d774-4a04-9858-2c58f77c93fd", + "key": "f0e0a8c0-01df-47d7-92e5-c3c16e962f4f", "params": { "message": "Wait until user intervention" } }, { "commandType": "temperatureModule/deactivate", - "key": "bb2a6fad-2767-45ad-bc5f-bac249004c00", + "key": "bde12c91-d991-4d57-8d7b-172706f3aa2a", "params": { "moduleId": "0b4319b0-75c7-11ea-b42f-4b64e50f43e5:temperatureModuleType" } diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index bddc1313927d..5519ec4f5024 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1689346890165, - "lastModified": 1711047167434, + "lastModified": 1711742514037, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -179,6 +179,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "100", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "preWetTip": false, @@ -194,6 +195,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f9a294f1-f42b-4cae-893a-592405349d56", "stepType": "moveLiquid", "stepName": "transfer", @@ -205,6 +210,7 @@ "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "mix_mmFromBottom": 0.5, @@ -222,6 +228,8 @@ "dropTip_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin", "nozzles": null, "tipRack": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "5fdb9a12-fab4-42fd-886f-40af107b15d6", "stepType": "mix", "stepName": "mix", @@ -3753,7 +3761,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6e489e69-6adb-4874-ad9f-4da035825829", + "key": "17a2f6e6-dc06-4c3a-8e97-52728d96dbd5", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3762,7 +3770,7 @@ } }, { - "key": "7536b20c-1416-4e5a-9e0a-2ac13f805fcd", + "key": "23762a87-4d05-4ce1-adaf-b2e7288bfef9", "commandType": "loadPipette", "params": { "pipetteName": "p50_multi_flex", @@ -3771,7 +3779,7 @@ } }, { - "key": "67136a9e-c10f-40ce-80de-920e33d78d44", + "key": "74ed5557-4813-4892-a2e3-4f7710b70d1c", "commandType": "loadModule", "params": { "model": "magneticBlockV1", @@ -3780,7 +3788,7 @@ } }, { - "key": "469e8246-7e19-4654-acdd-7c29a79ce67b", + "key": "00beb9a8-59c7-4c99-b386-0f4214d61350", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3789,7 +3797,7 @@ } }, { - "key": "29533de7-bd35-458c-9f60-6b9be67bd64b", + "key": "347f3697-2728-4c24-9067-8e9b7d9bd1d6", "commandType": "loadModule", "params": { "model": "temperatureModuleV2", @@ -3798,7 +3806,7 @@ } }, { - "key": "85e3ebf2-4d2f-49e2-8335-cf8c69d58372", + "key": "89c6d0b5-71ed-4bf9-9d94-15375788b86a", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3807,7 +3815,7 @@ } }, { - "key": "d05c0cc2-d6c2-4fd3-9918-33f7d07bd2fd", + "key": "07ba1a3a-9161-47ee-bf63-501e847bc84d", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 Flat Bottom Heater-Shaker Adapter", @@ -3821,7 +3829,7 @@ } }, { - "key": "7b515b7c-9d35-4d4e-a19c-1a73de8fdc65", + "key": "c9aafdba-c777-4609-b99f-87405a76a7ec", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Filter Tip Rack 50 µL", @@ -3833,7 +3841,7 @@ } }, { - "key": "583e9796-64e7-411e-b3e4-ce3c5f18a39a", + "key": "008af3b3-4557-4755-af65-4e263bcd4d52", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 100 µL PCR Full Skirt", @@ -3847,7 +3855,7 @@ } }, { - "key": "bb089d2b-b8f8-4306-b0b8-e5d38d81aba6", + "key": "df64c3d8-c74b-468e-b663-f88c59ed927c", "commandType": "loadLabware", "params": { "displayName": "Opentrons 24 Well Aluminum Block with NEST 1.5 mL Snapcap", @@ -3861,7 +3869,7 @@ } }, { - "key": "aa80f4db-d94f-407c-9ea1-6df86119d200", + "key": "23249708-2910-493b-aa56-a05e687f13ee", "commandType": "loadLabware", "params": { "displayName": "NEST 96 Well Plate 200 µL Flat", @@ -3876,7 +3884,7 @@ }, { "commandType": "loadLiquid", - "key": "52dfe64f-29b5-4d3e-838d-aecf6c0df8e0", + "key": "46b4c996-8800-432b-824a-9f9fb2ae033e", "params": { "liquidId": "1", "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", @@ -3885,7 +3893,7 @@ }, { "commandType": "loadLiquid", - "key": "68e4b018-1e5b-48d4-b858-93b3154e63a5", + "key": "b8e21e25-5da0-426b-a1da-8d87751e48cc", "params": { "liquidId": "0", "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", @@ -3903,7 +3911,7 @@ }, { "commandType": "temperatureModule/setTargetTemperature", - "key": "18dcba87-324d-4483-a9be-e561c9b47bf0", + "key": "0b60938b-1bd4-4ffb-89f6-dac42a87ac0e", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType", "celsius": 4 @@ -3911,7 +3919,7 @@ }, { "commandType": "heaterShaker/waitForTemperature", - "key": "84ae2f28-38f6-4314-9d34-3ff9af5a875c", + "key": "7d5fd109-43cd-4dea-b0fb-2efa3f727e38", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "celsius": 4 @@ -3919,14 +3927,14 @@ }, { "commandType": "thermocycler/closeLid", - "key": "0ec55f7b-4f82-46ad-a450-aac71d8ca198", + "key": "31bb9bbe-9c53-407a-ac73-e789b800466d", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetLidTemperature", - "key": "883d4fba-7b4c-410d-aeff-79ce4c3d106e", + "key": "0d83be22-5cec-4603-b42c-03ffb6e6d8ba", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "celsius": 40 @@ -3934,14 +3942,14 @@ }, { "commandType": "thermocycler/waitForLidTemperature", - "key": "8da2d0c4-c9b2-4acb-8230-6b68f33b92b7", + "key": "1ac36b4e-b0df-4d43-9cfc-a10cc64ccda3", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/runProfile", - "key": "6df23192-439c-429c-89ab-c932751096f0", + "key": "0917c6de-9fd8-4afa-b496-f62ae18fa290", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType", "profile": [ @@ -3953,28 +3961,28 @@ }, { "commandType": "thermocycler/deactivateBlock", - "key": "c99151db-add8-41e4-9e5b-0516198f06b4", + "key": "4e5e9302-fac9-438d-83c9-fabd4c65791f", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateLid", - "key": "2cf60177-6b03-4706-a2fb-7a211eb974e1", + "key": "a0fe06fa-e4cc-4de2-97a9-388a3df08111", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "thermocycler/openLid", - "key": "968e4a04-1cda-4730-a26d-810c0af827ad", + "key": "8706cf32-b7c8-41ee-901a-6e62ef7b6824", "params": { "moduleId": "627b7a27-5bb7-46de-a530-67af45652e3b:thermocyclerModuleType" } }, { "commandType": "pickUpTip", - "key": "39f9b118-55f7-4b32-b28a-689255ecf69a", + "key": "90d3558e-e3ef-4e11-8e18-9e1312b212b0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -3983,31 +3991,37 @@ }, { "commandType": "aspirate", - "key": "cbd4de4c-8de3-407e-aace-9d0993c48214", + "key": "c7ac4218-4698-48f4-b00d-8eeb1ffddb3a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "df9dcd8b-54a2-4ea3-9014-8d1e5a8769c7", + "key": "604c9a1d-1ada-4159-850f-3bc9e4f802bc", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "41586872-d7ba-47a4-b545-aca0f924e1e5", + "key": "c120780c-b4f4-4b11-a7f6-ab3b2621106f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4017,12 +4031,12 @@ }, { "commandType": "dropTipInPlace", - "key": "1a253e2c-f44c-414b-929e-fb071233caa5", + "key": "2b9bb184-749e-4652-a2cb-31e427ae0472", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "903f0cbd-36fc-46ad-9629-af0e713a2551", + "key": "24425f50-40ff-453a-9c3e-ba35f07a4b93", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4031,31 +4045,37 @@ }, { "commandType": "aspirate", - "key": "15e1819f-a75f-4a99-b248-ef9c44f742bb", + "key": "3eacc9b8-99bf-448b-b178-1638c2217d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "1746f838-436c-4524-99ca-79a89807e7c6", + "key": "b2f71d3b-13b3-4ba5-9672-3a5ae85b402e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "139ac8eb-77bc-4dd5-8358-5cd3352b2841", + "key": "982eb315-0f07-4db4-804d-3650a7ef3371", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4065,12 +4085,12 @@ }, { "commandType": "dropTipInPlace", - "key": "7d3f53de-e3e9-4cce-aca3-a1c84ec8e4f5", + "key": "fd1e4fcb-3f57-4e0e-9a07-f5710d713b2b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "519b1444-568a-49af-87cc-d251a28d5c74", + "key": "d1aa96b8-8218-497f-92d1-9d145d65cacd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4079,31 +4099,37 @@ }, { "commandType": "aspirate", - "key": "eef96aa3-ed34-4a6c-a121-9bb1556016aa", + "key": "49b8562e-7d04-409e-b96e-60c04d82f890", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ee44942b-3da5-43c1-ad0b-17fc2ea46b68", + "key": "16da2628-d7fa-45e9-9911-cb06a61e488e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "1cad272e-a562-4abe-975a-209d5b29f6b8", + "key": "a7a1c2f8-6fdf-4322-a216-ca06fe064299", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4113,12 +4139,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b25eeb63-22a3-4a45-9a34-2588f6e28034", + "key": "dcfb2a3c-fec6-467e-8ea4-0655e070857c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "3b4257ba-7e37-4dc7-8630-628695a993b0", + "key": "c54b1b14-a78e-4b3b-a7fd-df600c143996", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4127,31 +4153,37 @@ }, { "commandType": "aspirate", - "key": "9ee178b8-6918-477a-ac4a-3b94b3257ded", + "key": "1f586aaa-a2c3-4f35-98d4-514f30f8afde", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "5b1fa15c-d50a-465e-99d6-d3c5631ba18b", + "key": "8491a928-c8ae-4b73-8fd3-43e6e520ea7d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8ca34094-5af4-4c19-bbc9-e6a95ebe7dc7", + "key": "3ddc68fb-3f9e-4395-b234-a8f00b35cf97", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4161,12 +4193,12 @@ }, { "commandType": "dropTipInPlace", - "key": "ed570c7c-bec9-4320-92c6-c5dc4e8ea039", + "key": "c1596fb8-587a-4a9c-9dd0-252dd821085c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "24af8dc9-1d39-4ead-9fb7-c106cf81c4ca", + "key": "9e130b45-4d49-4588-adef-2e4055be2e09", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4175,31 +4207,37 @@ }, { "commandType": "aspirate", - "key": "3fb7136f-156c-4ef2-ba9c-4010cbee7c45", + "key": "7e014576-f260-4b18-aad5-f45423adb35f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "70415053-47db-400e-a765-59930e782fba", + "key": "07e28184-9669-432a-9b68-8dd692680fa5", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "6bc262a7-6619-4c1d-90ab-694881833ef2", + "key": "4f591d38-4cc1-496b-90dc-fdcff81d3155", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4209,12 +4247,12 @@ }, { "commandType": "dropTipInPlace", - "key": "61c26a5d-05d6-490b-9881-2505f334e148", + "key": "fab6cdf0-a1c5-4643-9d0c-4fce01d88c7f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e7db6545-1f4f-40bd-a440-08afc48c8a6d", + "key": "7407659a-a612-4209-967b-af9750324a07", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4223,31 +4261,37 @@ }, { "commandType": "aspirate", - "key": "a6a1af44-5a04-4fdf-aecb-afec57d49809", + "key": "3d307bba-026c-4a9a-8d01-ae93e8cdce1f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "fa36f3c3-8a96-4643-adc3-1c856446c432", + "key": "f45088fb-f102-4edf-ad26-5d1d0ac4f215", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a3b84d03-7b16-4c8b-ab04-5e80d53c5008", + "key": "3b44aeec-fd56-4fcf-badf-5cdc42ed42c7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4257,12 +4301,12 @@ }, { "commandType": "dropTipInPlace", - "key": "b3b763fc-d538-4b1c-9a72-8a1c85238e55", + "key": "7a58db8b-f053-46b5-bd89-3a7cba9c1af1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "aeb3764a-c819-4151-acb9-77d07399b13d", + "key": "6449dbc6-430e-468c-863d-3233689c8a63", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4271,31 +4315,37 @@ }, { "commandType": "aspirate", - "key": "9b15d50e-e1a3-4966-8584-5e32da3afa4c", + "key": "fe2b869a-8d1f-47bf-9688-2deae97b30f9", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ada5949a-0085-4f09-b65e-db092a2ddab6", + "key": "e9a20fb6-f0ba-4e25-b1e5-67dbef00f2d0", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "fbfd8391-eb1e-416f-97b0-4ea4e631b8b5", + "key": "c0c7ae2d-6b13-4ce7-b170-5a2ffb3cc066", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4305,12 +4355,12 @@ }, { "commandType": "dropTipInPlace", - "key": "edc2c62c-4f1e-4b9d-958c-1f9d4594759b", + "key": "eea51b62-8fd2-4c34-8929-48e26c670640", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e9f79e00-bc1f-4f92-9299-401a7d85d78b", + "key": "0a59af4d-5196-4c16-b609-98c565c320da", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4319,31 +4369,37 @@ }, { "commandType": "aspirate", - "key": "f361f6e5-a558-48dd-b808-383d48305944", + "key": "cc1387fe-4e22-407f-b1f6-8e57153d24d1", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "ad9f20e5-c018-4241-8ee4-0b94bd4b4b13", + "key": "fdbb2c46-7e42-4dc9-95dd-528397fe2a49", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c41c44f9-7969-4b9d-966f-7133927d1746", + "key": "f8000789-3db0-4edc-adaa-234a89c0a2e8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4353,12 +4409,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e2deb31d-b531-46a0-95b9-894c6143b51e", + "key": "4a6423a4-3fb3-41cb-a2bb-769f882da188", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "ad15920f-80f3-4b31-8c69-7a9753760ac5", + "key": "58db6a04-8af4-4580-8b3b-71d27448d36c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4367,31 +4423,37 @@ }, { "commandType": "aspirate", - "key": "ee11c611-bd0f-4039-b631-738aceae4b8b", + "key": "6c053630-6298-4bae-8b1b-b7c0fd60cd64", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "dace1ad5-22a3-4731-8a0e-54378e936e41", + "key": "60bddd52-347b-4e97-af4f-227172c9e383", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c0075e01-b1d6-41fc-a482-541eba3dd9ce", + "key": "dcc5e7a5-ce62-40b0-94a8-19ccd9ec7783", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4401,12 +4463,12 @@ }, { "commandType": "dropTipInPlace", - "key": "287153c4-baee-46c6-81e0-db0cd90a8d7b", + "key": "2d0d4405-02e0-44d3-9aa9-093b2bcf8693", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "e956cb88-1387-4014-aea0-06237c9ea125", + "key": "8f943b62-e5cc-423b-962d-c9f06a3c39e6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4415,31 +4477,37 @@ }, { "commandType": "aspirate", - "key": "3d870b55-af49-42b0-b877-7a3777fea82d", + "key": "d38287f1-db91-4479-a811-6190c472a797", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "8d06248a-65a5-4372-a666-70ec956df1d4", + "key": "10074111-ee60-4602-8749-326cc7c978ef", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "3304feb9-69e8-4e70-a3b6-308706f06d0c", + "key": "f0b5078d-30e7-4ae8-bd9c-2380a2acc248", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4449,12 +4517,12 @@ }, { "commandType": "dropTipInPlace", - "key": "2f73eac1-8a7a-4bb2-b634-3c938bb6a541", + "key": "babbd4a6-95d0-46ef-9616-15435bf83e0c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "b08587bf-ac3a-4322-baa4-2d54ab4c74d8", + "key": "44873109-2a10-4925-a393-b3f05ac65cc8", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4463,31 +4531,37 @@ }, { "commandType": "aspirate", - "key": "627b7b39-4646-42e5-8a6b-ab85c073631a", + "key": "956b196e-e6a0-4e04-9fe4-e54e8f366cd3", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "f96068bb-8730-46d2-9649-e77060a67d96", + "key": "d9fe1d4f-558e-48e9-9c4f-3349a513da68", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "717da31c-723f-4761-8a9e-c46e3a0d95d9", + "key": "3410b8d0-d4be-4009-be92-13d7165fa45d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4497,12 +4571,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9b69289d-3b6c-4caa-ab71-eee65591d5d7", + "key": "b610b324-aa96-44ed-95d0-fa7b6b2771f7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "61f66087-286a-4309-8f56-e95e1d3450db", + "key": "37fe97fb-40d5-449a-ab57-995eb34db25b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4511,31 +4585,37 @@ }, { "commandType": "aspirate", - "key": "fcd20bdc-8cd3-4dc4-88ff-572317dfeafa", + "key": "f8fe5dca-1294-4f9a-8b05-7b818317070a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "cc4e76bc-f89a-45e5-8200-1bfd3ba3c950", + "key": "9ae4ac38-6188-4e0a-82b1-c8682052eab7", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "f98a9dd9-6368-45ad-bf34-374ebd304017", + "key": "5a3d6103-e920-419f-8541-6f42aead55b4", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4545,12 +4625,12 @@ }, { "commandType": "dropTipInPlace", - "key": "28f596a5-d900-4335-82d1-c69c2a4f3476", + "key": "ae7fa272-1052-4b9b-9141-832de7f191ae", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "cf3e0222-4b78-4c11-8f7c-853edb173ef0", + "key": "001e1eff-7e3a-4762-889b-81bbdd95624e", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4559,31 +4639,37 @@ }, { "commandType": "aspirate", - "key": "87bf3bc3-d986-4d4e-9303-531db931ecf2", + "key": "4c857bd6-9ee5-4abc-b8f1-93f263421d4f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "7d4a2f34-efc4-4c69-ac95-12e90da24846", + "key": "d54ee4e1-019a-4043-a9d6-73f2728ade40", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "c26ac9f4-f86a-410b-9524-3a107df76154", + "key": "b3d1a836-8198-4543-9c69-5af4340f5e7b", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4593,12 +4679,12 @@ }, { "commandType": "dropTipInPlace", - "key": "cc9ad177-40b1-4063-904f-bdcd5f534ba3", + "key": "d09a4c10-5d65-46b2-aa72-04ebd1e69616", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "6f27f601-b3a2-4d72-b1c7-d29fbef1e2fb", + "key": "a18317f2-d1e8-4960-8294-d041900be78c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4607,31 +4693,37 @@ }, { "commandType": "aspirate", - "key": "0d614bd3-9723-467a-a8f4-6793875071a6", + "key": "7b5f0098-2f53-4e57-b60a-46c06f4fe167", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "321bc387-8de7-4456-99d2-f61e31f49ac4", + "key": "e6497c4f-50da-481e-b76d-a6787df6a779", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "23c76ed2-6751-4f8d-b061-02e54bae4b92", + "key": "7c357bd8-9b73-43d0-a143-57d9b24d651f", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4641,12 +4733,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a33f1f9c-c1eb-42c5-b2d2-dc81d0c92207", + "key": "5ea9d3e3-5c64-4610-bbfc-b71d7e4d3282", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "fa8827df-09aa-41df-b2af-794dafab3f36", + "key": "17f52737-8fa4-45df-95e1-e95011c308fd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4655,31 +4747,37 @@ }, { "commandType": "aspirate", - "key": "222b1e4d-81fb-44c5-beff-d39ae967766c", + "key": "43f318b4-d316-462b-9d38-d4969cac5494", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "d6644b59-fd5b-44cf-a5b4-da4bec2ffcd1", + "key": "ed84c3b2-b095-49bc-939b-fd1f5faa6ddd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62a05dff-fd3d-47ec-a852-7ec365fdc60a", + "key": "bdde31de-c35d-403a-bc01-d249c21100dd", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4689,12 +4787,12 @@ }, { "commandType": "dropTipInPlace", - "key": "dcbbb300-64a6-4deb-9733-19b86574106a", + "key": "2d5caff3-718e-4835-90c1-3a0d2ec57a20", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "882ee6ef-f2a3-4478-b88a-fd650c11cbc9", + "key": "a9e33581-f053-47cb-9bc4-069dca4fbc1c", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4703,31 +4801,37 @@ }, { "commandType": "aspirate", - "key": "4745b4e5-0fed-427b-961a-c1e346e5f591", + "key": "5b34a48a-fdf2-4ad1-8c14-3da9ffb680ed", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "dispense", - "key": "916ce3bf-32a1-45e1-b704-32b75db4e572", + "key": "13cfa89d-7337-4358-86d2-0da34380835d", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "volume": 50, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "62fc2bbb-ee9f-4769-9c6d-7eae1d56b7f3", + "key": "ec94b555-dba0-4757-be27-7b8634c55a9a", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe", "addressableAreaName": "movableTrashA3", @@ -4737,12 +4841,12 @@ }, { "commandType": "dropTipInPlace", - "key": "459eed51-26e1-4f67-946c-5f281d9ff5c4", + "key": "efba76a3-5a32-4f02-9dfc-2f1e5ff3e9b6", "params": { "pipetteId": "2e7c6344-58ab-465c-b542-489883cb63fe" } }, { "commandType": "pickUpTip", - "key": "a11d26ea-07d0-4fbe-95a8-3229bf3a7974", + "key": "844f8618-5db6-48ba-b0af-ffc12e84eea7", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "labwareId": "23ed35de-5bfd-4bb0-8f54-da99a2804ed9:opentrons/opentrons_flex_96_filtertiprack_50ul/1", @@ -4751,7 +4855,7 @@ }, { "commandType": "configureForVolume", - "key": "8355523c-68f9-4d67-b7f0-c19b39a20d58", + "key": "5d899711-013e-460b-845b-9a8ef207dc24", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10 @@ -4759,55 +4863,67 @@ }, { "commandType": "aspirate", - "key": "cb6f6fd9-ce96-4788-930a-bcb4fc73cab8", + "key": "71923e56-ac8f-486c-9509-c809a994e006", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "d72adc76-8ece-47d3-86d2-5ccec5b507a4", + "key": "abde93a4-98e5-428c-9dd9-2a65dc3d99bf", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "aspirate", - "key": "aefc83b0-fcba-4010-b71a-9b79e6134779", + "key": "f9a0576f-5764-478e-bf16-03ef8ab46d3b", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "955873e5-8fcd-43d4-9f6c-3281922fc97d", + "key": "e1e8644f-f0d0-4946-b599-55d62174b5af", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "volume": 10, "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "a237c669-fe56-434a-a5fc-225ba5403b28", + "key": "9bb9217e-3c87-4b11-81f4-01aeb6d12bcd", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193", "addressableAreaName": "movableTrashA3", @@ -4817,12 +4933,12 @@ }, { "commandType": "dropTipInPlace", - "key": "87d1c5d5-c40e-480a-a0bf-4a8a63bae3cd", + "key": "dd0506d4-cd19-4fa3-85db-64aef25d8f75", "params": { "pipetteId": "6d1e53c3-2db3-451b-ad60-3fe13781a193" } }, { "commandType": "moveLabware", - "key": "98e741c6-01c6-43d5-8d98-e3950b7dabda", + "key": "bd579612-fa2a-4808-ade0-8e38b9d8b7da", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4831,12 +4947,12 @@ }, { "commandType": "waitForDuration", - "key": "da873c66-a7e4-4810-b2d1-ab037e9156d1", + "key": "da8a328a-2870-4259-b3be-89d3255154fb", "params": { "seconds": 60, "message": "" } }, { "commandType": "moveLabware", - "key": "58ced158-ae4d-4275-814b-cf29bacda1c0", + "key": "64ac3bcc-4ab8-4d15-9b42-d2462686153d", "params": { "labwareId": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4845,21 +4961,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "2706f0f9-5898-47e3-b53f-06df74598b4e", + "key": "cd0e65dc-cd6c-4d0f-b05f-3d8a979d7d09", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "073eac12-998e-4075-bcb5-feb215d5f251", + "key": "c980a10c-a99c-4583-831b-8f09f89822fd", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "37fedb11-cfa0-494d-905f-e5539c2960e6", + "key": "15a3aeed-9bd0-49d6-8a6e-43f226e7acfe", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType", "rpm": 500 @@ -4867,28 +4983,28 @@ }, { "commandType": "heaterShaker/deactivateHeater", - "key": "6431c63c-608d-4f82-bc68-ade76b8c1cc2", + "key": "001d2bdd-b8a2-4285-8aa3-9d9318566b47", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "a22834db-00b7-4f66-a681-9cc5dd17031e", + "key": "609e5b71-9dda-47d7-a7c4-0da3802e7e99", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "af548bbd-5620-405a-bda0-ac08ca06fbbd", + "key": "bb80d557-573b-4b09-a0b8-5d73ea22e4a4", "params": { "moduleId": "c19dffa3-cb34-4702-bcf6-dcea786257d1:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "ffb00dfa-99db-4744-9bde-538d0aa7b1a7", + "key": "a37c38e0-7abe-433f-ab9d-adf0774565f6", "params": { "labwareId": "a793a135-06aa-4ed6-a1d3-c176c7810afa:opentrons/opentrons_24_aluminumblock_nest_1.5ml_snapcap/1", "strategy": "manualMoveWithPause", @@ -4897,14 +5013,14 @@ }, { "commandType": "temperatureModule/deactivate", - "key": "8e0e035d-33a2-4995-a557-26c7951de915", + "key": "1558d15f-e4b6-48bb-8c9c-c3ff69812504", "params": { "moduleId": "ef44ad7f-0fd9-46d6-8bc0-c70785644cc8:temperatureModuleType" } }, { "commandType": "moveLabware", - "key": "36c8ae59-9d10-428a-b6fd-3e3b6b49ed09", + "key": "d805d58b-f6e7-406d-8262-5bf3d03448b6", "params": { "labwareId": "239ceac8-23ec-4900-810a-70aeef880273:opentrons/nest_96_wellplate_200ul_flat/2", "strategy": "manualMoveWithPause", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json index 79c866f5399b..2a0e6bcde5d7 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701659107408, - "lastModified": 1711047424926, + "lastModified": 1711742533084, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 21 Mar 2024 18:51:59 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -154,6 +154,11 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "9d61f642-8f9b-467d-b2f7-b67fb162fd26:wasteChute", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, + "blowout_z_offset": 0, "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", "stepType": "moveLiquid", "stepName": "transfer", @@ -3421,7 +3426,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "1809fd39-db28-4928-8773-31bc536fe765", + "key": "f8a4cabe-7cb9-4e38-b937-6655680e2a31", "commandType": "loadPipette", "params": { "pipetteName": "p1000_single_flex", @@ -3430,7 +3435,7 @@ } }, { - "key": "3a5f75b2-15c9-404f-9b87-f102beeb1a45", + "key": "cd2e6185-8d57-4881-9b0c-ebcbd2468c55", "commandType": "loadModule", "params": { "model": "heaterShakerModuleV1", @@ -3439,7 +3444,7 @@ } }, { - "key": "a13ba2f1-e557-4d2f-a304-87847ce68887", + "key": "b2d44cd2-73db-45b3-ab22-e9e765beed75", "commandType": "loadModule", "params": { "model": "thermocyclerModuleV2", @@ -3448,7 +3453,7 @@ } }, { - "key": "e3f1abb9-b076-4b56-a593-0b4033462fea", + "key": "bbd3ee7e-35b8-4168-9df5-13b871c6dfba", "commandType": "loadLabware", "params": { "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", @@ -3462,7 +3467,7 @@ } }, { - "key": "9d92792f-e5d1-4259-8e4b-da8ea83f28df", + "key": "198896f6-4d0e-49ee-b060-bc9d17fbb9bc", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", @@ -3474,7 +3479,7 @@ } }, { - "key": "d03df580-7915-4bba-9d34-e92039cfe24d", + "key": "880af66e-2905-4102-b655-0351b30252b1", "commandType": "loadLabware", "params": { "displayName": "Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt", @@ -3488,7 +3493,7 @@ } }, { - "key": "d1e4cf27-a1db-48c4-b784-a21014bb234b", + "key": "478e31cc-12f4-4a30-9cd4-03181a538513", "commandType": "loadLabware", "params": { "displayName": "Axygen 1 Well Reservoir 90 mL", @@ -3501,7 +3506,7 @@ }, { "commandType": "loadLiquid", - "key": "64129bfd-92d7-4c70-9380-33785a6041ff", + "key": "56bffeaa-ee2b-4cb8-91dc-a9e21e8f1655", "params": { "liquidId": "1", "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", @@ -3519,7 +3524,7 @@ }, { "commandType": "loadLiquid", - "key": "ac47f11d-0d9c-48d7-b45b-9ecb269a9a50", + "key": "e95ef8f9-fef7-4dfe-b5db-86a5dff7e5b5", "params": { "liquidId": "0", "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", @@ -3528,14 +3533,14 @@ }, { "commandType": "thermocycler/openLid", - "key": "bfa8af0c-4cb2-49d3-912b-b07e90a1f752", + "key": "63d31323-1217-4a56-9392-c1c28dc703d7", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "moveLabware", - "key": "a991e2d5-5be6-43b1-9a71-2f229aea392f", + "key": "716ec050-c597-490d-b261-20ac8e3b4c2f", "params": { "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "strategy": "usingGripper", @@ -3544,7 +3549,7 @@ }, { "commandType": "pickUpTip", - "key": "55826f7b-111e-4768-a6d3-d0a4c4a5e20d", + "key": "635b128e-5cdc-4bdc-9975-c04a49fb7670", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3553,31 +3558,37 @@ }, { "commandType": "aspirate", - "key": "bb5688fe-2909-4755-be74-1850d4d05735", + "key": "1a26a0e0-11c2-4940-b32d-8c747e6969a7", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "33f7aa0b-80e4-41f0-a841-d8aacb4c7f32", + "key": "17f82c54-3e03-46f4-9c65-666aacc5bab3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "650a3b63-379d-4327-ae55-9752d04497ab", + "key": "d38dc37e-e466-47c9-a7bc-85322487af8c", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3586,12 +3597,12 @@ }, { "commandType": "dropTipInPlace", - "key": "40c51d0f-5a80-4355-91c1-aaaba7489f37", + "key": "69952335-9a0e-4b69-a903-00454f162e8f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "006d7584-e3ad-43a9-8fa1-0688f1d74304", + "key": "2a6d6805-bb22-42c6-9d38-321bdbd9f941", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3600,31 +3611,37 @@ }, { "commandType": "aspirate", - "key": "562c0ad9-1f97-4e74-af40-107e12019e41", + "key": "087e94b5-a8f7-4637-a830-eb99e2d3a631", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "cbd55dd4-a746-4bf5-bf43-73afd95ebff2", + "key": "6edf7c6f-858c-4170-9b69-9f230144ba8a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "B1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "1bac0a50-7a55-4abe-905c-547f006fd62c", + "key": "129a19fb-6a84-4196-a712-7400142cfff2", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3633,12 +3650,12 @@ }, { "commandType": "dropTipInPlace", - "key": "480d48a6-b825-406a-bc6c-b95b457a1eba", + "key": "46e0edd9-a8eb-4dc4-840d-496ce6ecb732", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "aed7d916-7957-4608-8678-895cd03f2bb8", + "key": "2c31e97a-5821-4fd9-b171-d29ac18cda36", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3647,31 +3664,37 @@ }, { "commandType": "aspirate", - "key": "6c2a45d8-449f-4d46-858d-01c349ec7481", + "key": "c5d54202-b261-497f-aa71-3bbdb73f2441", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "2259e5af-9e35-45bc-b869-105e0d6bda3e", + "key": "df57bdd7-104c-4923-a561-002043500c74", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "C1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4422ed17-8cf6-47f4-b945-352f17a81fb0", + "key": "eddd8f7b-ccd6-4919-885d-bf20bbbc675f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3680,12 +3703,12 @@ }, { "commandType": "dropTipInPlace", - "key": "33bf2ffd-b472-4d01-a063-e6d78cd10f6e", + "key": "2f5e18c4-1436-47f1-9010-975fe41ca901", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "0b9fe44a-1d94-48ed-9d52-058fb8639425", + "key": "c4508229-340b-42af-850c-f8d4d10caeae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3694,31 +3717,37 @@ }, { "commandType": "aspirate", - "key": "d617d4ec-ae3c-4517-acea-7ff57af655ef", + "key": "7b548807-dd81-479e-a00f-b4cd9d2080ff", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "99bf9993-2553-4adc-9131-be9fe370b9df", + "key": "8d8053f6-f155-416c-986c-1893f87d979f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "D1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "4f05b8d1-319d-40b5-a006-31a41ad5742f", + "key": "92fa7df4-7cd5-42fd-8405-7baf417b46e3", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3727,12 +3756,12 @@ }, { "commandType": "dropTipInPlace", - "key": "8faee0ed-2458-45d7-b09f-8021317417cd", + "key": "b2cc5f6e-dc14-4a5e-8f54-1fbcf779e850", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "bf3176ac-63db-4218-8042-d5683092a66d", + "key": "149f4bc1-ecb0-49c8-bf2a-9e1dc7d241dc", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3741,31 +3770,37 @@ }, { "commandType": "aspirate", - "key": "fbea3f6f-0421-428a-bf21-6cda35b30407", + "key": "43ee041e-de88-4f88-8d40-700334aaf355", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "fedd8c6f-777b-4913-afd9-63c919394a5c", + "key": "779c450d-0d43-4b71-aa73-5f29ed51f5dd", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "E1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "a1934186-6d8b-4fdf-b17a-8f9e93f63417", + "key": "b2be4778-5e00-4bc1-8431-cdecb7ad74ad", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3774,12 +3809,12 @@ }, { "commandType": "dropTipInPlace", - "key": "4fb7ea89-471a-47c4-8af8-0a6bfdae1d74", + "key": "4fa0e93d-1f79-4af5-9bbf-c0e41f131053", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "f66afc4e-9476-4ca4-9cdc-a66257031413", + "key": "77a07fa4-8e68-49c2-aad8-74f04328a34b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3788,31 +3823,37 @@ }, { "commandType": "aspirate", - "key": "a629a9e7-e34f-4693-8479-3cb27d44d0b6", + "key": "06c28a5b-53c6-4aa5-89e0-30b509d2c68f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "f831d4dd-c2c2-4429-9314-2fbef18546d6", + "key": "0caa3ced-9327-48aa-b59f-07ea65a81702", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e05ddba8-7f1b-45a6-a8d9-9de8b01146bc", + "key": "592051e7-385f-49eb-aeb2-aca173c7e8d4", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3821,12 +3862,12 @@ }, { "commandType": "dropTipInPlace", - "key": "059f01dc-eb9f-4cfd-92cf-0b67113e4c2d", + "key": "10c97227-329e-453d-bc1c-16b929cc7ad5", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2d85f593-c882-45b2-89ec-f3bd9cd7c645", + "key": "a85a3cb6-68e8-43d4-8c87-218bca8fe3ae", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3835,31 +3876,37 @@ }, { "commandType": "aspirate", - "key": "860d1800-6f8d-46d6-a939-81569e9641fc", + "key": "8804e9b7-b0e6-4814-bf38-48a5b05fb106", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "1d9ef0b0-926e-446c-b0df-c57dfc97f34e", + "key": "5cf8eaf7-c60d-41e2-bb90-c10b3dcb092f", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "G1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "c2954781-c45e-46ff-a8fa-36faea77630c", + "key": "f3e72ab1-d7ea-4857-aa42-8f25b2ec5d1b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3868,12 +3915,12 @@ }, { "commandType": "dropTipInPlace", - "key": "05db8e46-e6c4-4039-84ca-cf7a11042eb9", + "key": "2a0395ec-7363-407b-a391-e8e361d5098b", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "pickUpTip", - "key": "2cbabc82-4412-4bc5-a7d2-12b74b39b641", + "key": "3246289c-9e03-43d4-8451-e6736a8a709d", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", @@ -3882,31 +3929,37 @@ }, { "commandType": "aspirate", - "key": "9f9e94a0-4a33-441c-8864-e64f9a0fda07", + "key": "470b2170-edec-412a-beeb-56de7f85c0ea", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "dispense", - "key": "d2144ca8-ca39-484a-a8a0-9c70e613be8a", + "key": "dec80858-857c-4ca9-89d1-235affcdfbc8", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "volume": 100, "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "wellName": "H1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 160 } }, { "commandType": "moveToAddressableArea", - "key": "e8f7d982-7346-4e25-81b1-98e0412553d2", + "key": "998c55f5-86d6-4ba3-ac30-33d818357753", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", "addressableAreaName": "1ChannelWasteChute", @@ -3915,19 +3968,19 @@ }, { "commandType": "dropTipInPlace", - "key": "880baa31-8fdb-4e11-9183-d90052fca1e2", + "key": "47eadfc8-8244-4509-9462-2fa624b8488a", "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } }, { "commandType": "thermocycler/closeLid", - "key": "e1c31c80-51e8-47db-be63-29d861843b56", + "key": "15e90989-96e1-4e86-9381-d56db11b7659", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/setTargetBlockTemperature", - "key": "ae59fc04-b753-482e-87f0-8680cdccb6c4", + "key": "0dc52334-283f-458d-91a7-3b19c722a8f6", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", "celsius": 40 @@ -3935,47 +3988,47 @@ }, { "commandType": "thermocycler/waitForBlockTemperature", - "key": "66261c91-97d7-4170-b2f6-462ad85b660e", + "key": "78800364-855d-467f-8f52-8838892375d2", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "waitForDuration", - "key": "d09638c2-a49c-4b38-b22f-d581fb68feca", + "key": "264eed35-aa11-454f-83e1-3771ca54b87a", "params": { "seconds": 60, "message": "" } }, { "commandType": "thermocycler/openLid", - "key": "b5017439-7aa1-483a-a475-3b03ce1a4505", + "key": "80009058-c8ad-4da4-80da-9167e79188aa", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "thermocycler/deactivateBlock", - "key": "b9e78735-3881-4493-82cb-4bd628bd288d", + "key": "e8109b8f-f380-44b5-965a-40867be7765b", "params": { "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "af91feaf-c12a-4059-abbf-91d33820a1c0", + "key": "389a88e8-7267-4cd8-bd5b-22e86d06150d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "7a7352ad-9879-4b2e-bc48-540ac0b2ad3b", + "key": "de12dc4b-89b8-42be-801d-02b70e3b04ff", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "a2fe52a9-4acf-4599-afc3-5ed26bd579a8", + "key": "8822ab1b-89a9-4b0c-abac-1e3abb792d63", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -3986,21 +4039,21 @@ }, { "commandType": "heaterShaker/closeLabwareLatch", - "key": "b057b4d6-57ae-4443-b798-2e6d9103c2e5", + "key": "91e9ed0e-4d2e-4eb9-b49b-0e30e5b5ea9d", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "cb6fba71-dfd4-468b-a351-22279bfad1c1", + "key": "1c03bbae-0989-4d1a-87c9-ee73003298ab", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/setAndWaitForShakeSpeed", - "key": "a8c7211e-11f9-41e2-b977-68b513a2db5d", + "key": "af3f5cbc-801c-425f-a4c7-04c5bac0826c", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", "rpm": 200 @@ -4008,40 +4061,40 @@ }, { "commandType": "waitForDuration", - "key": "91be8718-5404-496e-957d-011a33f9cfe0", + "key": "af1c659a-fcbb-46aa-9c1b-6f233dee281e", "params": { "seconds": 60 } }, { "commandType": "heaterShaker/deactivateShaker", - "key": "11592571-7419-4880-b987-ace8edf90b8a", + "key": "ca120664-8293-4e0f-b8fd-2feb4c75cbf9", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "3da43478-0315-4c19-aaf2-087b174e1ecf", + "key": "abb2cb21-1848-4b51-a769-0bb74b8b0aa0", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/deactivateHeater", - "key": "7038cc9a-87e9-4554-a69d-3828d1cf9273", + "key": "bd384e07-ddc3-430b-aa2d-04c9b874b130", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "heaterShaker/openLabwareLatch", - "key": "4a2c5d7f-31c0-40ff-ab77-b5eb167f4008", + "key": "25b0e4d1-ebd9-419f-ba55-691724c6ab66", "params": { "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" } }, { "commandType": "moveLabware", - "key": "b2f3676b-da1a-411e-b106-fc761a5ce11b", + "key": "26c1f526-457b-46c2-9fe6-30fd595feabc", "params": { "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", "strategy": "usingGripper", @@ -4050,7 +4103,7 @@ }, { "commandType": "moveLabware", - "key": "f76dccda-5917-48cf-97eb-efd0ae2138f2", + "key": "b64778b0-86e3-495a-809d-90a4a636c3ff", "params": { "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", "strategy": "usingGripper", diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 531adb047e9d..56b9885aea92 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -6,7 +6,7 @@ "author": "Author name", "description": "Description here", "created": 1560957631666, - "lastModified": 1711650670235, + "lastModified": 1711902162091, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Thu, 28 Mar 2024 18:30:23 GMT", + "_internalAppBuildDate": "Sun, 31 Mar 2024 16:22:18 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -114,7 +114,8 @@ "disposalVolume_checkbox": true, "disposalVolume_volume": "1", "blowout_checkbox": true, - "blowout_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "blowout_z_offset": 0, + "blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "preWetTip": false, "aspirate_airGap_checkbox": false, "aspirate_airGap_volume": null, @@ -126,8 +127,12 @@ "dispense_delay_checkbox": false, "dispense_delay_seconds": "1", "dispense_delay_mmFromBottom": "0.5", - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", "stepName": "transfer things", @@ -139,6 +144,7 @@ "labware": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": true, "blowout_location": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "mix_mmFromBottom": 0.5, @@ -153,9 +159,11 @@ "dispense_delay_seconds": "1", "mix_touchTip_checkbox": true, "mix_touchTip_mmFromBottom": 30.5, - "dropTip_location": "a1a3a3ee-84f5-44f2-b6c5-015be69c0208:trashBin", + "dropTip_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin", "nozzles": null, "tipRack": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", + "mix_x_position": 0, + "mix_y_position": 0, "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", "stepType": "mix", "stepName": "mix", @@ -3336,7 +3344,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "6f4d2d94-4cab-4ead-9827-36a729f06652", + "key": "818878e2-9a2b-498e-be2d-1d317f6f7af8", "commandType": "loadPipette", "params": { "pipetteName": "p10_single", @@ -3345,7 +3353,7 @@ } }, { - "key": "fc0d5cb8-d53b-4629-abf6-b0935b8b4812", + "key": "1ae8e180-58c4-4970-b372-9a8f1869f297", "commandType": "loadPipette", "params": { "pipetteName": "p50_single", @@ -3354,7 +3362,7 @@ } }, { - "key": "a7c0b1ac-b2c6-4e2a-9e4a-b6a7787b48f9", + "key": "ce9f8375-8577-4062-a9ff-12bc33d3bec5", "commandType": "loadLabware", "params": { "displayName": "tiprack 10ul (1)", @@ -3366,7 +3374,7 @@ } }, { - "key": "55d92dea-5339-4f0b-b771-fb1089f281ed", + "key": "8f2f7622-476b-40ff-b692-768a69158aa2", "commandType": "loadLabware", "params": { "displayName": "tiprack 200ul (1)", @@ -3378,7 +3386,7 @@ } }, { - "key": "bfdd6d43-a127-48a5-9bd2-0e2693edf78e", + "key": "6802ec5e-204e-4a63-87a9-c6066788e537", "commandType": "loadLabware", "params": { "displayName": "96 deep well (1)", @@ -3391,7 +3399,7 @@ }, { "commandType": "loadLiquid", - "key": "73a8dbd7-47e8-441a-8c8f-1dbeee57241d", + "key": "c63af547-a330-4e04-96ea-f04ef3c93ca1", "params": { "liquidId": "1", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3400,7 +3408,7 @@ }, { "commandType": "loadLiquid", - "key": "c1c5b6cf-8bb5-49a0-b887-2a4b0cddfefc", + "key": "d1af9a18-bb2f-4929-b952-7b1e21eadac8", "params": { "liquidId": "0", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3415,7 +3423,7 @@ }, { "commandType": "pickUpTip", - "key": "ee2227a1-11d2-447c-b97b-9079725370ca", + "key": "24f9ab3b-48fd-42cb-8e0d-2128427459fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3424,91 +3432,112 @@ }, { "commandType": "aspirate", - "key": "7dccc871-ae50-4281-a55d-71628dd2475d", + "key": "426ca672-56a0-430d-bdba-23632ad728b0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b42680f-634b-47b5-95b9-293d73ef6f4a", + "key": "ea2eab58-723d-462e-ae8b-d0daa9462ece", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c3a5efec-5dfc-41ce-98c2-983f31ca659d", + "key": "fa061fa1-e5d5-42cf-b9dd-d4b9a6b6eabe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5ce878dc-f20f-40fc-89d0-8b5551028f5a", + "key": "1e21ebe5-4e6f-4bc5-8dc3-1f1aa9158ff5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7ee113bc-9d41-470d-a909-bffb2510d00f", + "key": "7ad7bdad-84eb-42a0-b4ac-48949808a041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "24cf716a-a105-4817-838c-817755dbb986", + "key": "dd723bb6-9eba-4ab6-bc80-03f6f6db17df", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "dbd9da16-cf9e-44ea-aabe-b6a0bf4f7a60", + "key": "eddfcde7-5497-42e7-bff4-56d2052bc552", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "7e8803d0-9788-4780-94fa-3a336747cb5a", + "key": "e3e8b3d6-a118-43de-9155-7d1a1da67dbd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3518,67 +3547,82 @@ }, { "commandType": "dispense", - "key": "7af6bccd-f70d-40fd-9026-146d24f45606", + "key": "080a9a26-92ba-48ba-84ed-0a10743b7918", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a2b37a9d-35ee-4151-ae60-221144efeaf9", + "key": "ac6f0caf-5fe8-4d45-9659-1265fd022295", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "85d9b492-381f-4412-b9de-9343fabd06e2", + "key": "b9e03bec-0741-4dc9-b953-cadd7e7c40b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "3eab4b4b-ff8f-4e99-a98e-0e8f181aaca1", + "key": "017fd13a-0e3a-4f54-94c3-8d5fc8eb4ba4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "83f8b202-581a-416b-863d-5cbc19d5cfdb", + "key": "7961e88d-1b9b-4615-bcbd-31320a03f81c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "0f0d93dc-f745-46e5-a75d-7d939503d930", + "key": "4d60aa9f-e59b-491a-b494-aef4b877f6fa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3587,7 +3631,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d3468c15-f33f-4bb2-aed2-571abb2a0195", + "key": "8bf8312b-7058-430e-8344-84ed35dda280", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3595,7 +3639,7 @@ }, { "commandType": "touchTip", - "key": "243f8f03-d546-48f3-8641-439e79bfef83", + "key": "728468cd-08a9-4811-b5a8-ce0649835d29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3605,7 +3649,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "631c7562-faa3-4ee2-95d7-6bdbefaec4bb", + "key": "1b5e20e3-85d5-4d87-89f9-7d9568696f6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3615,12 +3659,12 @@ }, { "commandType": "dropTipInPlace", - "key": "fc391196-ed70-44b5-ba11-8abae97462eb", + "key": "3f520a13-6e4f-4ade-bbf8-2fdd35b875c3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "9e8eb31c-3e34-4281-aa18-5de6d0cd195e", + "key": "4b8db7a7-609e-431a-bf9d-7cf858c4b8f7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3629,91 +3673,112 @@ }, { "commandType": "aspirate", - "key": "c5e81bdb-1992-40f7-869a-ff0325c199de", + "key": "0355948e-57ca-4572-baa5-7a64b7ef28cc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "8c0865e8-0d42-4f15-8b70-845e5d9b45fa", + "key": "193a745f-0698-4427-8d0d-d1e4fe24de24", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "97c816e1-3045-4f09-bc33-150e256cde65", + "key": "8d205199-aa0a-4640-9a23-b3adcca61be2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "06d2d102-35f5-468d-b23c-900bd1df2789", + "key": "fe86a1bb-8c8e-4307-b06e-c92a8e231679", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7556aad7-86b4-4606-a5cc-5f7f7b56f0d9", + "key": "1976e9d0-ee3f-4ca0-a039-147dd8c21399", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "edbbed85-7ab1-4aad-a603-06654028c9d0", + "key": "b75876f5-cbf6-43ae-8bb5-1b71641ccc6a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6ccfd5ad-d683-48e6-a4db-fd911a6803be", + "key": "c6ff48bc-a06c-4e5b-9172-986375d8a934", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "6b77c1fa-dbb6-4933-b04c-c043b8f183ac", + "key": "7a15666d-4676-41b5-8752-26cc8a07f17e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3723,67 +3788,82 @@ }, { "commandType": "dispense", - "key": "1ab3a31c-ee75-4918-a89c-443b6a160d9b", + "key": "ec56b383-c163-402e-9996-d4cc69a1cffd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "d38199b9-9ea1-4994-8124-af29d5bacd69", + "key": "cabfdd05-1309-43e2-bfbd-d04bc7de85c9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "39df2363-edb6-4b3f-9226-3e1e40f49a83", + "key": "05cb631d-9092-46e9-b802-6175fbae1e1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "05046dbb-2bd5-4d5f-9029-592630619967", + "key": "ea50ada1-23d9-4ecf-af9d-3246930afd26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "04bd6a9a-012a-49cb-ba87-e96e3b42febc", + "key": "2523b9ed-ef76-40c9-8947-18c039e50939", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4381a5c3-9f62-44f0-9030-cecbd7116762", + "key": "58c4751a-5628-4596-a171-1ac260259c28", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3792,7 +3872,7 @@ }, { "commandType": "blowOutInPlace", - "key": "bca261f2-6071-4457-a47c-2bb76109e746", + "key": "ba5016a9-cd7a-41c8-bf17-aadb64664190", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -3800,7 +3880,7 @@ }, { "commandType": "touchTip", - "key": "5f923682-3cae-4d33-9dfc-29ac10adb4ae", + "key": "1314e2d9-8d46-4663-9bf3-458a300b0add", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3810,7 +3890,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "65c6a620-3fbb-41f0-b185-91c6fa6dbda6", + "key": "8527d992-4185-4f20-99a9-864541aaa7b6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3820,12 +3900,12 @@ }, { "commandType": "dropTipInPlace", - "key": "d063d2b8-234c-4e38-b66a-85a4011cbf94", + "key": "8c564bbd-34dd-44d2-ace8-995097f571b9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "432b72d6-f0c8-4cea-8bc2-b98fdae69445", + "key": "5377f188-8a31-4ff3-8ed3-ff5b651e467b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -3834,91 +3914,112 @@ }, { "commandType": "aspirate", - "key": "62c975b5-3adc-4900-9119-a87d8f7098b6", + "key": "70c291fd-f5c9-4216-9446-de8191fff376", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "10139a39-fb4a-4080-88ca-ebe511cb2d56", + "key": "7f1299ec-8930-457d-a2d9-c18876da3769", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bc81ba4-9b04-4e2c-88b7-f75f6c3dd3ec", + "key": "d04dee6f-90a4-4b4b-89b8-05f1104431fd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "bf7df5f4-1c18-46b7-b8f1-cc0853d1244a", + "key": "c983ed9b-783b-411a-8df2-50ef254b4deb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b86464a1-ee5d-4fce-b073-f14730bff0aa", + "key": "678dc318-94d9-488b-b2e3-f04ed29a2863", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9e4fb406-bf5e-4571-b4e5-dfb1ff8f2b98", + "key": "6aee8385-14b4-48fa-bef0-3a642d38c1cd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fb8ce4d1-79f9-4ddf-b11e-ebed2414333b", + "key": "c9e9500e-5c89-450c-a56e-7058720a74ce", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4be36f15-e8e3-4d6b-84b7-fe64db61ead3", + "key": "eeabdbf7-0dda-4246-859f-de8b643184c0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -3928,67 +4029,82 @@ }, { "commandType": "dispense", - "key": "e3fdb442-d127-4b6d-8829-b688b55397a6", + "key": "60f965e4-60af-4183-99de-15c77232416d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "55c1e1fa-78a6-4605-b6b5-8953cbbf7010", + "key": "7a40b467-9754-4c02-ae2e-4644cb997555", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "61620e17-2c1f-4a35-a64c-ef224b5b2a52", + "key": "a24675b2-41c7-4908-97ce-6bcf04c3d149", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "39adc386-6ab8-4664-a0f4-5196f475e19f", + "key": "71a467a6-4c67-46e1-b829-f9a02fb6669e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e5349e3a-6d1f-481a-8f37-6716b88d93a5", + "key": "b58fb6c6-17f0-44cf-add2-5ad3a99a06fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C8", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "89ae9ed8-0d2c-4b64-af9c-cf0c7bda3fd9", + "key": "b97a7e69-13c0-444b-9405-c84d8ab431bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -3997,7 +4113,7 @@ }, { "commandType": "blowOutInPlace", - "key": "6ac0f84b-1da9-41e7-a9e7-e5d7c5823077", + "key": "7e767220-28ab-4b59-ae54-1df3a59ac491", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4005,7 +4121,7 @@ }, { "commandType": "touchTip", - "key": "0e94a3f7-0bc1-42ea-bf18-b03b600ec548", + "key": "a4329dfb-0547-498b-a132-5314bdc37453", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4015,7 +4131,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "4e04ac60-3844-4f1c-afcb-753d8efa8073", + "key": "222528ae-afc3-459f-bd12-291fb6e92977", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4025,12 +4141,12 @@ }, { "commandType": "dropTipInPlace", - "key": "9c27a051-f55a-4859-9ee0-12cb2e4cc127", + "key": "a2b1c413-6b6d-4db7-b39f-36e801bb67bf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "83b37a71-e721-4454-98ba-a0e4c3311b06", + "key": "ee7cca8e-9d5a-4308-b437-91b3ac59e95c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4039,91 +4155,112 @@ }, { "commandType": "aspirate", - "key": "193df488-664a-4f29-8d62-4165930cde80", + "key": "9c65eb65-086b-4535-8dd4-fcdc3b1ce711", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "81ab7f7a-4c7b-4b74-9749-9cd2d146716c", + "key": "de99e84e-c816-42d7-bbaf-c685cf196c84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "96680762-7d73-4c16-98d4-6ae783afd729", + "key": "2bb3b611-e413-4866-9f88-2093be26c559", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "1be89f92-b2bd-4e14-b230-4e72ebc6fc77", + "key": "51c61ed1-215a-4304-b0bc-f7c0787d9759", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f693e0d2-1aff-4dc7-b6e3-cdd6ef614c01", + "key": "a5cb7070-9db9-4d93-94a0-baafdb9e1246", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "7918d2ba-e312-438c-8f15-ca28e8724bae", + "key": "b4812aa0-2c04-4f9f-a060-dcddb31655eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "8ac0c540-ff45-4cfd-995b-3fb6870ba09f", + "key": "09657153-451a-4ce8-a0aa-d238e97b5d4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4b930577-57af-4907-859a-f54bc71dc58d", + "key": "1ba61ffa-26f7-4258-806e-459483f8aee2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4133,67 +4270,82 @@ }, { "commandType": "dispense", - "key": "cdb0573e-5982-40c7-95c2-4d884d69a313", + "key": "3e54188d-9608-4976-b2a8-0262bc6cd9a8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "6a3055ee-44ca-43fd-b1ea-caac89343321", + "key": "12abbaa6-4354-4635-86c7-53da228b89e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "d7d8b056-6979-4840-aa44-b527e116aeff", + "key": "75989dac-fb90-46e0-8510-05946f0bb820", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "54dcd384-1fad-4071-bd6f-8f09a4eebb3d", + "key": "970cd398-3ad1-46ee-a917-9781c74964c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4710bca2-6bb3-4d86-8a27-192c431b525a", + "key": "224042a5-8347-4867-b30c-ea349eee0eb0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "d3d0b4cd-c86a-43b2-99b0-9c9818dca0f3", + "key": "5bca8d87-fae2-4082-92f1-5da5e9b0b01a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4202,7 +4354,7 @@ }, { "commandType": "blowOutInPlace", - "key": "621e6320-03b1-4d3a-82f9-000c120042ce", + "key": "6a40c11f-2894-4c0d-ae8c-3069aa7a3ac6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4210,7 +4362,7 @@ }, { "commandType": "touchTip", - "key": "23c285de-7aa1-4a16-a457-015e2fb7abb7", + "key": "9667d8ab-87f8-4af8-a61c-39fa46e15928", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4220,7 +4372,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "b0268cc2-ae71-4f29-94cb-032b56e36252", + "key": "54efaffe-8b67-45b0-8a1b-34eb9929230b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4230,12 +4382,12 @@ }, { "commandType": "dropTipInPlace", - "key": "980de7a4-b9ad-40c5-af04-a989bf3ff807", + "key": "4732e9c8-8b22-447d-9e8a-04360782f50c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "3e68bb44-ba33-484e-88cc-c931435e0c48", + "key": "55fbea4b-e8d2-4cc9-84f1-e531eedc46c8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4244,91 +4396,112 @@ }, { "commandType": "aspirate", - "key": "b02f553c-c223-4fb7-8899-7db1a60186d0", + "key": "d735d944-73ff-4713-ac51-c1341e5cc1a9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b012ac38-0070-4ff4-acfe-d42b6c5f9674", + "key": "33e8c95b-801c-42c3-9048-fa14b6aa7f29", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "537bf097-77dd-43bd-a67b-77a146f5349e", + "key": "b25b278a-8b01-4bc2-a1f8-456c7bf8c526", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2931c986-44ae-4ba2-bb36-bd705feb875c", + "key": "23d673c8-d769-480b-858b-43ac62636220", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "bc5af19c-0dd5-4791-a5b1-34002997cb3d", + "key": "3452e515-d862-40d0-99e1-34dd0404337f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "dc5d2b3e-8efd-41a9-b84e-d7debee06ac9", + "key": "36c73f15-d9cd-410c-8699-f19396584618", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0de5e018-4a9f-4cfe-9f58-f50901663c3c", + "key": "7b78234d-4513-49cc-83e7-10b662ff8675", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "af42fb71-74da-41b8-9b50-41048e949434", + "key": "b1b3ee6f-a9be-4220-8004-7296970de788", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4338,67 +4511,82 @@ }, { "commandType": "dispense", - "key": "72efd216-e92f-4103-a71e-85be208865ec", + "key": "f7c5a31f-1a71-478f-a145-eb5c5c567c6d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0387ffa2-40c4-4280-86d1-8c1fd39b6356", + "key": "5e4a8c3c-5a80-488b-898d-d1074f2c426c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c09fbe67-3e6c-4f82-8bc5-25db6a3d5a50", + "key": "da0e8d29-8619-47e1-b8da-98ccaf2c56fc", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "1939bb32-88c2-4d55-bb3f-7d31535a3403", + "key": "3fd622c1-93bc-4e5d-92cb-3dc40f38d92d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "b3bdd7bd-5cc2-42fa-b938-24fbc32931d6", + "key": "7fe8ecbf-6872-4c45-9f41-b3f5e31b8c42", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "ff827e3d-8136-44c1-a29a-33e0a0abf081", + "key": "7e7f40a5-1b19-414d-b1ec-b0f632ee81eb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4407,7 +4595,7 @@ }, { "commandType": "blowOutInPlace", - "key": "ee65b14e-529d-4116-81a7-ff50f28bc1a4", + "key": "e7d928ca-d918-43a1-973a-e56361029dcd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4415,7 +4603,7 @@ }, { "commandType": "touchTip", - "key": "e31dd584-a774-41c7-9176-62749596b7e6", + "key": "1665f0f5-1778-49ed-a765-bcdcc3a9c13a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4425,7 +4613,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "cd354a43-9b7d-48d5-8e2a-f6c369ac10f4", + "key": "5f71c216-2dd4-4b3f-9958-feac1e0ba419", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4435,12 +4623,12 @@ }, { "commandType": "dropTipInPlace", - "key": "a435f546-520d-4e38-bc22-f5f084f95d5d", + "key": "0d98fee0-4ada-4ddd-98cc-ee4f51763615", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "ba2e7ee3-715f-4588-93e8-05d4b1eed1cc", + "key": "1eca1b12-6dda-4a57-84cc-48ed09a5dcc7", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4449,91 +4637,112 @@ }, { "commandType": "aspirate", - "key": "1c2d5f90-6dbd-4b61-b97e-a4bf38f056d9", + "key": "6468842b-d755-431a-8f39-63390afc45aa", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2fbc684b-57c7-4e89-8d53-85c7f6f806de", + "key": "3f19926d-5262-4869-8830-7eb13951f4fe", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ef0f4077-3692-41b0-ad2d-0bcf94a1a075", + "key": "0816f07a-7ddf-41da-91a8-6c55bcf902ff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "a386c011-855b-4f41-be57-623647498c1a", + "key": "6ac9d9b6-b45e-4b0a-90c5-835a680ab914", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "9dd98cc7-2557-48bd-baf9-2e54ab47883c", + "key": "2c0b977d-cc77-44bb-b0a3-62339279f8d4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "3d57ac48-bf99-498b-b523-1be901efcc1e", + "key": "b15ab048-c8ae-491b-ba0a-ddb84af43b8a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "e64a4b94-cd07-4eb2-9edc-ab83093fc4bc", + "key": "ebb52c59-bc4d-4f3a-b1b4-10ceea23ecd4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "99567709-ebe8-4244-8252-dedb5aeb666c", + "key": "1c48b0b0-c786-4278-a95b-180d8bc8d7fb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4543,67 +4752,82 @@ }, { "commandType": "dispense", - "key": "94901710-b6db-4d27-b893-71108cc6186c", + "key": "6db1da99-4bfc-4723-a37b-db57a913a5a0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0467798b-8ec8-4d1e-afee-2a73a8422bcd", + "key": "b040900a-f61c-462e-9238-87746a45c0b8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "fc858419-1723-4c54-85d1-2d2ef53637ee", + "key": "8e2de19c-a6b1-4af7-a614-8f692815d667", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b30572d5-f396-41f3-8662-a4285508710d", + "key": "a72f4e61-2874-4af0-a471-d97434970e2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "5da67cc2-d056-4d4c-abc5-3a70269c38bc", + "key": "ff833f33-6c7e-417a-8293-f9a2c2eead8c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C7", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "c847fdfb-bb85-4335-9821-8fded3c15f0e", + "key": "40d74de4-9953-43ae-b4bc-518d39005303", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4612,7 +4836,7 @@ }, { "commandType": "blowOutInPlace", - "key": "d269fea9-b30e-488f-a2b2-37ae88547251", + "key": "7570e6a2-b2a3-4836-aaa0-13c90ceb08f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4620,7 +4844,7 @@ }, { "commandType": "touchTip", - "key": "da6ec212-7d12-411a-9f2b-2beff5ed197d", + "key": "5de67294-430d-4856-aa25-0177b32ef514", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4630,7 +4854,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "d8bef8c0-954a-4293-a75f-2589a37fc982", + "key": "b25ac8f3-fe61-4f87-b5f2-40936132a6dd", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4640,12 +4864,12 @@ }, { "commandType": "dropTipInPlace", - "key": "784c0470-5513-4f60-bb7e-f039db7b170f", + "key": "aa3d17b8-8d52-462f-9e39-b0d2d83e5407", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "5729228a-64f0-443e-91fe-31179efbdd1a", + "key": "188da1f2-486b-4dfd-b2c8-e0903544fa8d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4654,91 +4878,112 @@ }, { "commandType": "aspirate", - "key": "288895f0-14c7-4909-a300-178801bd08b4", + "key": "df11a136-0f66-4502-ad52-443adc71ca2b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "2f718ed4-0d72-45c4-bb4d-cc8265cbbf9a", + "key": "00502ab3-b649-4532-ba39-184ff41b00cb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7fde7d76-b68e-43d2-a00f-3203fdcfd95e", + "key": "cdc0749e-e66b-480e-afe0-3ad6c5e739e4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "ca2845d1-33bf-49cf-8bfe-48bbe544419e", + "key": "65529980-e475-4f51-a8dc-cd1f7e5a5020", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b5b15b72-dce1-430e-8050-e5e4b6fd9d54", + "key": "d9e94497-0439-4675-bb57-cc2e62ea7a84", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "c3bc77de-b5d0-43fa-a7a5-bc9e6b6fd765", + "key": "27bd35c9-4ef4-471f-954b-289db56992ad", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "c08f49f2-c0c8-488b-beab-160ad57f46c5", + "key": "9241c560-e1d0-4468-ac78-10c9511d0113", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "1d53f469-6c0b-4264-bb92-abb8299f650d", + "key": "67e511d9-8198-4c0d-808e-c9600f2aff6b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4748,67 +4993,82 @@ }, { "commandType": "dispense", - "key": "122d4fc9-e63d-430e-8ea0-6c1b17c3f1a7", + "key": "ea876b75-dbb7-445e-afb4-efa1fd12eda8", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "7d9df411-c0a1-4e91-8716-c80643cbd868", + "key": "7551fb8d-3899-42f4-ba52-9e03c2410ae5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "73a6bb03-d083-475d-99de-452fb093e44b", + "key": "dae940af-8337-439f-83c5-39745994b216", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "818098d4-ddd1-4853-875f-eeaf28898e12", + "key": "d9c4b87f-8e3f-415b-9c61-b14cff73fa6e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "4b43d7c0-d2cc-4721-8675-98c0357889fd", + "key": "6e1ae4be-0622-490d-811a-1442a54f38c6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "E6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "4461238e-6823-489c-9b95-59529d34c5e6", + "key": "2172c551-8f66-49ec-b092-3cecb3ecd1e6", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4817,7 +5077,7 @@ }, { "commandType": "blowOutInPlace", - "key": "abcefb59-b32e-4b9e-8ac3-fb8589565405", + "key": "70f94de0-45c2-4082-85c7-000a3c7d4e05", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -4825,7 +5085,7 @@ }, { "commandType": "touchTip", - "key": "6e1c8052-ebab-401a-a3de-1a20d61a1b40", + "key": "7a8c6027-3547-4415-97e2-e4a8839cefcb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4835,7 +5095,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "72caf8d6-745c-4bb8-997b-c6b2685935b6", + "key": "9e76549d-de35-4be7-b42f-83e81eb148e5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -4845,12 +5105,12 @@ }, { "commandType": "dropTipInPlace", - "key": "e4f6c6e4-58b0-466c-972a-56ee8b56735c", + "key": "edb7a124-0334-41a3-b82f-237bf2a63e37", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "a5de52b2-a015-4377-9adc-2e784a8a3514", + "key": "f040345b-250f-4fa6-abc0-62e27fe59938", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -4859,91 +5119,112 @@ }, { "commandType": "aspirate", - "key": "f46ecf37-8a53-4f96-87b4-45b58807c754", + "key": "cd942842-7300-40c1-87a6-28f073ea3dc5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "cbedd7bd-637c-4767-a6a6-694b76138850", + "key": "f6a45b15-269b-482d-983b-d3bc5db57d26", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "a200a845-574f-4f0b-9ad7-39f095b6d732", + "key": "7d61c0b4-4555-435c-b837-b559b360a82e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "96a8c1d7-bd45-44a7-ba7c-44b4d1067f4e", + "key": "9f9dfc52-5ca3-42e2-b9d5-3bfa8521de49", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "b1aae64c-98fe-402a-8a6e-38046dc2d375", + "key": "11346b4b-af47-46f0-9461-52664eec0d39", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "9ec5ab4b-5da0-4859-b713-e849f806a4c7", + "key": "23982cac-52ae-484f-b3e7-c52c029b1e9a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "f6849f46-5724-4643-92bf-b526f5e263fa", + "key": "148dd2de-1425-482f-8fec-32731007bbff", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "9dc1c842-947a-4e0f-8601-bae5edf58bd0", + "key": "41e664b1-6199-4a33-9857-76df944f516d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -4953,67 +5234,82 @@ }, { "commandType": "dispense", - "key": "f9c66ebe-764a-4d16-975a-b9d275f7e6e3", + "key": "152340ce-cde0-469e-9882-a8ef3d4a1cde", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "ccf1ab0e-c50f-4c41-9eb9-5f84ec9c8d8c", + "key": "e4e8529f-89fc-4a94-a49d-410b799aa539", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "0f734cf9-c9d8-40ee-82f7-a34d97e43ed9", + "key": "01461514-1395-4f09-95db-29dea71c1f5b", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "0139e4ec-529e-4080-8926-37c140621866", + "key": "ff195ab9-cb65-45d1-93a8-a071d0bbed98", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "e3a5a1bf-0a24-4787-b3fe-2f60075de339", + "key": "8ba714b7-bcc2-48c3-8c57-0d0ac933b976", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "D6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "7b9216cc-c1d4-469e-a5d8-7683a943bb0c", + "key": "8c2017b4-9145-46bc-a91f-83f27cc0a828", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5022,7 +5318,7 @@ }, { "commandType": "blowOutInPlace", - "key": "cdeb2bad-74b0-4160-9984-ebb55bb04bc3", + "key": "6dba0671-c83f-4fc2-8d9c-3e309448d0e9", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5030,7 +5326,7 @@ }, { "commandType": "touchTip", - "key": "8d938cfa-0484-4692-bac6-143f3f52e75b", + "key": "15c49bf0-ce06-4687-aeb5-a5dd0736f2f5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5040,7 +5336,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "ec4eb309-173d-452e-a601-6ea966a7254e", + "key": "5e494f88-ee95-42f1-bbd4-23b449649b93", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5050,12 +5346,12 @@ }, { "commandType": "dropTipInPlace", - "key": "f92c4c88-0208-44f5-81b9-056546a45e49", + "key": "e1f4d20a-b36c-4da1-9b1f-529aef638f1f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "1ede6001-67c7-4d54-b866-4eb2d9b1d82b", + "key": "c3d944d3-abe8-4f4c-8e4d-70792c3303f2", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5064,91 +5360,112 @@ }, { "commandType": "aspirate", - "key": "ccf06eed-b517-4b14-b31d-736dcdc8c3b4", + "key": "4432786d-94e4-4958-ae49-8d0679c97fc0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "49f837ff-5dd9-4f54-b4a0-ffd492c4c969", + "key": "3efc13e5-aac5-4f23-b060-52003c8c827f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "530ffdc6-b112-4f88-b25e-745bb9c86516", + "key": "5ec72861-9ac4-4a9b-91e2-907932819e58", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "515f7c58-c506-4bad-95c4-4adfcdadea5d", + "key": "994b0746-ea15-4cfb-afa7-d00ff124e0f1", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "fc5e49e8-cadb-4a8a-addb-4525a0640254", + "key": "2acee0bb-366c-4f1d-b165-f69a1c03b05f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "26838934-eaf7-4a76-bb3b-070e3aab3bcb", + "key": "a44857c1-e5d2-4ce7-a428-41a68e426f3c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 2, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "eef5c160-b9b0-43cf-8e8e-9b836431a606", + "key": "09f55bdd-61ff-4667-878f-c79e0a21b9c5", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "touchTip", - "key": "4e84dbb8-53b6-400f-8530-eb2ee326dc13", + "key": "4daa0f4c-e10e-488e-9d19-3a8602a548f4", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5158,67 +5475,82 @@ }, { "commandType": "dispense", - "key": "19c3e661-d308-4544-babf-fd4cefd23331", + "key": "5f54be1c-fff2-41ae-b512-01a9bb28cc4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 6, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "2bfda325-1526-4178-8fa5-338c9dc9d92b", + "key": "6e42ea13-01ed-461b-8dfa-9bd360982ddf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "adabc3a8-3e76-423d-949e-8d5146862421", + "key": "63d6f42e-0caa-47c4-9341-e3a950f85128", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "aspirate", - "key": "957dda98-4628-4029-90bd-d1a2e0c280c3", + "key": "c8791232-20bd-4068-a778-4630548b49ae", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 0.6 } }, { "commandType": "dispense", - "key": "538985a7-8e67-4abd-94cf-68387fd80e7d", + "key": "98e4d5e2-4b75-435f-8809-099806e98694", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 3, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "C6", - "wellLocation": { "origin": "bottom", "offset": { "z": 2.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 2.5, "x": 0, "y": 0 } + }, "flowRate": 10 } }, { "commandType": "moveToAddressableArea", - "key": "71eaf4c8-c8a5-400b-b094-46bdcaa60daf", + "key": "921371a0-2df9-4f3e-b28f-0282399e98a3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5227,7 +5559,7 @@ }, { "commandType": "blowOutInPlace", - "key": "f935fe77-d02e-4bb8-95e2-5f25e8312dad", + "key": "f9c7ae2a-b401-4c92-8e6a-4366ffb93643", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "flowRate": 1000 @@ -5235,7 +5567,7 @@ }, { "commandType": "touchTip", - "key": "6cbaaafd-f358-4779-9cb2-3622e3285ae1", + "key": "70fbf7e3-cae6-49e7-bfd3-65a5376b5e3e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5245,7 +5577,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "756c761d-66fe-4fc7-8e53-cf258c4b95c4", + "key": "74d53fee-f9c6-4a27-a54b-80a79e906b6c", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5255,12 +5587,12 @@ }, { "commandType": "dropTipInPlace", - "key": "bfb11f03-bef0-4d98-a569-b21249c1f447", + "key": "28dc2329-937d-4d2c-8fc3-eecf3f321041", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "pickUpTip", - "key": "7dec52bf-9c68-42ca-838b-1ebb9c4f325f", + "key": "5ad18635-8559-4904-8db4-4e2b19546238", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "c6f4ec70-92a5-11e9-ac62-1b173f839d9e:tiprack-10ul", @@ -5269,90 +5601,108 @@ }, { "commandType": "aspirate", - "key": "83e518c4-7a06-439f-b7f8-175feb33b528", + "key": "1227b40e-adda-4545-9724-5509ff790adf", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "a79b3bc6-6e2c-4800-adf2-72f5b221e2d4", + "key": "b9c1000c-c52f-4b04-9790-9a2dec7dadd3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "3fd83532-cf51-43d6-bd74-ca3fcd09f175", + "key": "0b5da711-8961-40d0-a294-b4d9eed6c77a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "48d023af-e120-4d61-8eb0-76a9433258a4", + "key": "12b3c883-f2b2-4651-816e-e38bb8cb5c85", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "aspirate", - "key": "54f4aba0-c8f0-463d-8bff-8e3311db6765", + "key": "b30463df-33e7-4038-97d6-298f7e9cef8e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 8 } }, { "commandType": "dispense", - "key": "1589b195-68ec-47a4-baee-f27de214ef10", + "key": "b2c2c14c-6874-406a-b9d1-33bc02b7a74f", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "volume": 5.5, "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "F1", - "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 0.5, "x": 0, "y": 0 } + }, "flowRate": 7 } }, { "commandType": "blowout", - "key": "0a4211db-4a8c-496a-9098-0a8547f4e39f", + "key": "98f8d095-46f4-4349-8c93-21eebfcf05d3", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", "wellName": "A1", "flowRate": 7, - "wellLocation": { "origin": "bottom", "offset": { "z": 41.3 } } + "wellLocation": { "origin": "top", "offset": { "z": 0 } } } }, { "commandType": "touchTip", - "key": "0575a144-4887-4ccd-b64a-a1a18094a2f5", + "key": "d6985dc6-551c-4ceb-bcc9-c833301b1eac", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well", @@ -5362,7 +5712,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "2551d68a-3a19-4283-84d9-fd285ee0f745", + "key": "cdf5e0f0-0598-4e4d-98e8-70a57ff83a4a", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", "addressableAreaName": "fixedTrash", @@ -5372,12 +5722,12 @@ }, { "commandType": "dropTipInPlace", - "key": "981b6c74-860e-4c14-bb74-25c66d110508", + "key": "1c0dee1c-97fa-4f33-bb36-9b3b7a2ef73e", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e" } }, { "commandType": "waitForDuration", - "key": "a45e4cd0-d4b1-4042-9295-396f0e6b92df", + "key": "d306df0a-3ad2-48ac-9ac2-1151895982e0", "params": { "seconds": 3723, "message": "Delay plz" } } ], diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index 0cf5bc6679f2..6ace9e709266 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -6,7 +6,7 @@ "author": "", "description": "A test for 5.0.0 -> 5.1.0 migration", "created": 1600714068238, - "lastModified": 1709303322125, + "lastModified": 1711742569351, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -58,6 +58,7 @@ "labware": null, "mix_wellOrder_first": "t2b", "mix_wellOrder_second": "l2r", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "mix_mmFromBottom": 0.5, @@ -75,6 +76,8 @@ "dropTip_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin", "nozzles": null, "tipRack": "f1c677c0-fc3a-11ea-8809-e959e7d61d96:opentrons/opentrons_96_tiprack_10ul/1", + "mix_x_position": 0, + "mix_y_position": 0, "id": "fc4dc7c0-fc3a-11ea-8809-e959e7d61d96", "stepType": "mix", "stepName": "mix", @@ -2125,7 +2128,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "87303141-a159-4390-ab9e-c737b5e29d2a", + "key": "3004b46c-2b41-4453-8ddc-1629ec3b5249", "commandType": "loadPipette", "params": { "pipetteName": "p20_single_gen2", @@ -2134,7 +2137,7 @@ } }, { - "key": "1dbb2e54-da06-4512-b02c-b3a4c2fc539f", + "key": "c318feee-5ec6-40a0-9ecc-554e67b30ce1", "commandType": "loadLabware", "params": { "displayName": "Opentrons OT-2 96 Tip Rack 10 µL", @@ -2146,7 +2149,7 @@ } }, { - "key": "7c5e3453-255c-4216-a5c3-7787fa4ef106", + "key": "3350dee6-aa60-4569-a801-0dfeb5baf8ed", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2159,7 +2162,7 @@ }, { "commandType": "waitForDuration", - "key": "929f2a92-418b-411d-aa33-27db0788e1ff", + "key": "797e70f3-5310-48c2-ba06-12adb92a7b4e", "params": { "seconds": 3723, "message": "" } } ], diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json index abc2d2231769..702945f0b8cd 100644 --- a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -6,7 +6,7 @@ "author": "", "description": "", "created": 1701805621086, - "lastModified": 1709303384383, + "lastModified": 1711742604736, "category": null, "subcategory": null, "tags": [] @@ -15,7 +15,7 @@ "name": "opentrons/protocol-designer", "version": "8.1.0", "data": { - "_internalAppBuildDate": "Fri, 01 Mar 2024 14:22:27 GMT", + "_internalAppBuildDate": "Fri, 29 Mar 2024 20:00:04 GMT", "defaultValues": { "aspirate_mmFromBottom": 1, "dispense_mmFromBottom": 0.5, @@ -78,6 +78,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, @@ -93,6 +94,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "ALL", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "83a095fa-b649-4105-99d4-177f1a3f363a", "stepType": "moveLiquid", "stepName": "transfer", @@ -129,6 +134,7 @@ "dispense_touchTip_mmFromBottom": null, "disposalVolume_checkbox": true, "disposalVolume_volume": "5", + "blowout_z_offset": 0, "blowout_checkbox": false, "blowout_location": null, "preWetTip": false, @@ -144,6 +150,10 @@ "dispense_delay_mmFromBottom": null, "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", "nozzles": "COLUMN", + "dispense_x_position": 0, + "dispense_y_position": 0, + "aspirate_x_position": 0, + "aspirate_y_position": 0, "id": "f5ea3139-1585-4848-9d5f-832eb88c99ca", "stepType": "moveLiquid", "stepName": "transfer", @@ -2233,7 +2243,7 @@ "commandSchemaId": "opentronsCommandSchemaV8", "commands": [ { - "key": "e09dc6e2-c0e6-4b28-9460-865c48a3b03f", + "key": "7224d1a7-a7b3-4bb3-bc5c-65aa98565616", "commandType": "loadPipette", "params": { "pipetteName": "p1000_96", @@ -2242,7 +2252,7 @@ } }, { - "key": "3dc22b4a-9fa8-4c61-843d-b45a4054490e", + "key": "dcddeb3c-66d9-4868-9f9f-fbd47d754fc4", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack Adapter", @@ -2254,7 +2264,7 @@ } }, { - "key": "0f3b11ad-a015-4ece-9267-0ca57c832bfd", + "key": "c206434e-aa1e-44ee-8667-29accd89941a", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2268,7 +2278,7 @@ } }, { - "key": "0194f4bc-e114-4048-af3f-e053db83a79e", + "key": "3cdba839-f0fa-4e50-8399-94338cced032", "commandType": "loadLabware", "params": { "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", @@ -2280,7 +2290,7 @@ } }, { - "key": "c807c9aa-7300-40be-817f-6d2018cd9d95", + "key": "7f75bf03-3036-4847-afbf-4bbefdf6cee8", "commandType": "loadLabware", "params": { "displayName": "Opentrons Flex 96 Tip Rack 50 µL", @@ -2293,7 +2303,7 @@ }, { "commandType": "configureNozzleLayout", - "key": "131fd37b-29cb-41f8-8792-b3c210e2db36", + "key": "2326c781-0416-4319-b954-16929077b5e3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "style": "ALL" } @@ -2301,7 +2311,7 @@ }, { "commandType": "pickUpTip", - "key": "d08a4b16-f17e-4146-adff-68d3235f3174", + "key": "86f7ac25-739d-4a38-8bf4-4730a8e6cce7", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2310,19 +2320,22 @@ }, { "commandType": "aspirate", - "key": "79c1655a-54de-4c5d-8b74-3d866244b229", + "key": "0113e27d-0949-4305-8f0b-5467753dfac3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A1", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "e95fefc8-1738-4e24-89ab-e8b27fbde04b", + "key": "79c134c0-5042-4243-8a81-95ad54594ab3", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2331,7 +2344,7 @@ }, { "commandType": "dispenseInPlace", - "key": "432061e5-a407-43cc-b703-25882875ae58", + "key": "2ce5b534-62b3-4415-bdd6-747fb57545be", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2340,7 +2353,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "8e2ba800-c7af-451a-b730-0ef9115b970f", + "key": "7212407e-0bd1-4ef5-a8c7-4c6f95cee357", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2350,12 +2363,12 @@ }, { "commandType": "dropTipInPlace", - "key": "0cced503-95fa-49fb-8540-2d528819f20d", + "key": "55286f40-e2c1-44f6-a3f3-032bfbf89f3d", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } }, { "commandType": "configureNozzleLayout", - "key": "48a2d952-d9ad-4ed7-9021-31c97c43b175", + "key": "47ab8f5c-a2dc-40e0-a6db-3c2ff6c48778", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "configurationParams": { "primaryNozzle": "A12", "style": "COLUMN" } @@ -2363,7 +2376,7 @@ }, { "commandType": "pickUpTip", - "key": "474ddf94-384e-4c01-acbd-50e43c005c7c", + "key": "c6f563fd-4f3f-4bd8-833e-3519c4fb0026", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "labwareId": "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1", @@ -2372,19 +2385,22 @@ }, { "commandType": "aspirate", - "key": "1e082d08-89b8-4e5f-b80f-e9190280fad7", + "key": "ee919504-5c21-40c5-9205-00e8aee06718", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", "wellName": "A7", - "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "wellLocation": { + "origin": "bottom", + "offset": { "z": 1, "x": 0, "y": 0 } + }, "flowRate": 6 } }, { "commandType": "moveToAddressableArea", - "key": "42daf0a1-9c17-4c9a-b8e6-90e68e166d1a", + "key": "6c1dbdec-0d3a-4693-810b-b28984382fce", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2393,7 +2409,7 @@ }, { "commandType": "dispenseInPlace", - "key": "6e36d0e4-e975-4cf6-8dd4-24d74f9d60f7", + "key": "d7ad2bf5-3033-4168-adf4-082306dc5467", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "volume": 10, @@ -2402,7 +2418,7 @@ }, { "commandType": "moveToAddressableAreaForDropTip", - "key": "918fec4b-1947-49c5-8fe1-af24fef2bf3f", + "key": "9ca4968e-0995-4354-95a1-37964599784f", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", "addressableAreaName": "movableTrashA3", @@ -2412,7 +2428,7 @@ }, { "commandType": "dropTipInPlace", - "key": "7b5a5ab4-5dbd-4338-890f-38551bd58c4a", + "key": "548bbf90-da13-4487-a878-dd363b17d906", "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } } ], diff --git a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx index 062052ea9d67..76074bc8e3bb 100644 --- a/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx +++ b/protocol-designer/src/components/BatchEditForm/BatchEditMix.tsx @@ -88,7 +88,10 @@ export const BatchEditMix = (props: BatchEditMixProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > { const { labwareOnDeck } = props + const labwareEntities = useSelector(getLabwareEntities) + const adapterId = + labwareEntities[labwareOnDeck.slot] != null + ? labwareEntities[labwareOnDeck.slot].id + : null + const highlighted = useSelector(getHoveredStepLabware).includes( - labwareOnDeck.id + adapterId ?? labwareOnDeck.id ) let isTcProfile = false diff --git a/protocol-designer/src/components/EditModules.tsx b/protocol-designer/src/components/EditModules.tsx index 9df9defbdd92..7a4ef5b48c72 100644 --- a/protocol-designer/src/components/EditModules.tsx +++ b/protocol-designer/src/components/EditModules.tsx @@ -1,14 +1,21 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' +import { + FLEX_ROBOT_TYPE, + TEMPERATURE_MODULE_TYPE, +} from '@opentrons/shared-data' import { selectors as stepFormSelectors, actions as stepFormActions, } from '../step-forms' import { moveDeckItem } from '../labware-ingred/actions/actions' +import { getRobotType } from '../file-data/selectors' +import { getEnableMoam } from '../feature-flags/selectors' +import { EditMultipleModulesModal } from './modals/EditModulesModal/EditMultipleModulesModal' import { useBlockingHint } from './Hints/useBlockingHint' import { MagneticModuleWarningModalContent } from './modals/EditModulesModal/MagneticModuleWarningModalContent' import { EditModulesModal } from './modals/EditModulesModal' -import { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' export interface EditModulesProps { moduleToEdit: { @@ -27,6 +34,12 @@ export const EditModules = (props: EditModulesProps): JSX.Element => { const { onCloseClick, moduleToEdit } = props const { moduleId, moduleType } = moduleToEdit const _initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) + const robotType = useSelector(getRobotType) + const moamFf = useSelector(getEnableMoam) + const showMultipleModuleModal = + robotType === FLEX_ROBOT_TYPE && + moduleType === TEMPERATURE_MODULE_TYPE && + moamFf const moduleOnDeck = moduleId ? _initialDeckSetup.modules[moduleId] : null const [ @@ -74,16 +87,24 @@ export const EditModules = (props: EditModulesProps): JSX.Element => { enabled: changeModuleWarningInfo !== null, }) - return ( - changeModuleWarning ?? ( - + ) + if (showMultipleModuleModal) { + modal = ( + ) - ) + } + return changeModuleWarning ?? modal } diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 3049f036b4a7..11b8d21053d2 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -129,6 +129,7 @@ function getWarningContent({ const pipettesDetails = pipettesWithoutStep .map(pipette => `${pipette.mount} ${pipette.spec.displayName}`) .join(' and ') + const modulesDetails = modulesWithoutStep .map(moduleOnDeck => t(`modules:module_long_names.${moduleOnDeck.type}`)) .join(' and ') @@ -169,12 +170,14 @@ function getWarningContent({ if (modulesWithoutStep.length) { const moduleCase = modulesWithoutStep.length > 1 ? 'unused_modules' : 'unused_module' + const slotName = modulesWithoutStep.map(module => module.slot) return { content: ( <>

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

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

@@ -234,9 +237,9 @@ export function v8WarningContent(t: any): JSX.Element { return (

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

) @@ -347,7 +350,7 @@ export function FileSidebar(): JSX.Element { content: React.ReactNode } => { return { - hintKey: 'export_v8_protocol_7_1', + hintKey: 'export_v8_1_protocol_7_3', content: v8WarningContent(t), } } diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index ebe86be63a7e..827af5a2aa8c 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -1,7 +1,11 @@ import * as React from 'react' import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { fireEvent, screen, cleanup } from '@testing-library/react' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + LabwareDefinition2, + fixtureTiprack300ul, +} from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { createFile, getRobotType } from '../../../file-data/selectors' import { @@ -17,11 +21,8 @@ import { import { toggleNewProtocolModal } from '../../../navigation/actions' import { getHasUnsavedChanges } from '../../../load-file/selectors' import { useBlockingHint } from '../../Hints/useBlockingHint' -import { - getUnusedEntities, - getUnusedStagingAreas, - getUnusedTrash, -} from '../utils' +import { getUnusedStagingAreas } from '../utils/getUnusedStagingAreas' +import { getUnusedTrash } from '../utils/getUnusedTrash' import { FileSidebar } from '../FileSidebar' vi.mock('../../../step-forms/selectors') @@ -30,15 +31,14 @@ vi.mock('../../../navigation/actions') vi.mock('../../../navigation/selectors') vi.mock('../../../file-data/selectors') vi.mock('../../Hints/useBlockingHint') -vi.mock('../utils') - +vi.mock('../utils/getUnusedStagingAreas') +vi.mock('../utils/getUnusedTrash') const render = () => { return renderWithProviders(, { i18nInstance: i18n })[0] } describe('FileSidebar', () => { beforeEach(() => { - vi.mocked(getUnusedEntities).mockReturnValue([]) vi.mocked(getUnusedStagingAreas).mockReturnValue([]) vi.mocked(getUnusedTrash).mockReturnValue({ trashBinUnused: false, @@ -74,6 +74,13 @@ describe('FileSidebar', () => { vi.resetAllMocks() cleanup() }) + it('renders the file sidebar and exports with blocking hint for exporting', () => { + vi.mocked(useBlockingHint).mockReturnValue(
mock blocking hint
) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + expect(vi.mocked(useBlockingHint)).toHaveBeenCalled() + screen.getByText('mock blocking hint') + }) it('renders the file sidebar and buttons work as expected with no warning upon export', () => { render() screen.getByText('Protocol File') @@ -91,19 +98,54 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Your protocol has no steps') }) - it('renders the unused pipette and module warning', () => { - vi.mocked(getUnusedEntities).mockReturnValue([ - { - mount: 'left', - name: 'p1000_96', - id: 'pipetteId', - tiprackDefURI: 'mockURI', - spec: { - name: 'mock pip name', - displayName: 'mock display name', + it('renders the unused pipette warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: {}, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, + }, + }, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused pipette') + }) + it('renders the unused pieptte and module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: { + pipetteId: { + mount: 'left', + name: 'p1000_96', + id: 'pipetteId', + tiprackLabwareDef: [fixtureTiprack300ul as LabwareDefinition2], + tiprackDefURI: ['mockDefUri'], + spec: { + displayName: 'mock display name', + } as any, }, }, - ]) + additionalEquipmentOnDeck: {}, + labware: {}, + }) render() fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused pipette and module') @@ -140,4 +182,55 @@ describe('FileSidebar', () => { fireEvent.click(screen.getByRole('button', { name: 'Export' })) screen.getByText('Unused gripper') }) + it('renders the unused module warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused module') + screen.getByText( + 'The Temperature module specified in your protocol in Slot A1 is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.' + ) + }) + it('renders the unused modules warning', () => { + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + moduleId: { + slot: 'A1', + moduleState: {} as any, + id: 'moduleId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + moduleId2: { + slot: 'B1', + moduleState: {} as any, + id: 'moduleId2', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + }, + }, + pipettes: {}, + additionalEquipmentOnDeck: {}, + labware: {}, + }) + render() + fireEvent.click(screen.getByRole('button', { name: 'Export' })) + screen.getByText('Unused modules') + screen.getByText( + 'One or more modules specified in your protocol in Slot(s) A1,B1 are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.' + ) + }) }) diff --git a/protocol-designer/src/components/Hints/index.tsx b/protocol-designer/src/components/Hints/index.tsx index af77a54193bd..6f5bafd25274 100644 --- a/protocol-designer/src/components/Hints/index.tsx +++ b/protocol-designer/src/components/Hints/index.tsx @@ -74,12 +74,14 @@ export const Hints = (): JSX.Element | null => {

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

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

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

) + case 'thermocycler_lid_passive_cooling': return ( <> diff --git a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css index 5e27c4358fbf..439dccbdf8c8 100644 --- a/protocol-designer/src/components/StepEditForm/StepEditForm.module.css +++ b/protocol-designer/src/components/StepEditForm/StepEditForm.module.css @@ -269,33 +269,6 @@ and when that is implemented. margin: 1rem 0 2rem 14rem; } -.engage_height_diagram { - width: 90%; - padding-top: calc(40 / 540 * 90%); - background-repeat: no-repeat; - background-size: cover; - - &:hover { - cursor: pointer; - } -} - -.engage_height_diagram_gen1 { - background-image: url('../../images/modules/engage_height_static_gen1.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen1.gif'); - } -} - -.engage_height_diagram_gen2 { - background-image: url('../../images/modules/engage_height_static_gen2.png'); - - &:hover { - background-image: url('../../images/modules/engage_height_animation_gen2.gif'); - } -} - .tc_step_group { margin: 1rem 0; } diff --git a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx index 6e8f91d1ec29..6637092deab4 100644 --- a/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx @@ -7,8 +7,8 @@ import styles from '../StepEditForm.module.css' import { FieldProps } from '../types' type BlowoutLocationDropdownProps = FieldProps & { - className?: string options: Options + className?: string } export const BlowoutLocationField = ( @@ -28,7 +28,7 @@ export const BlowoutLocationField = ( return ( (false) + const [targetProps, tooltipProps] = useHoverTooltip() + const labwareEntities = useSelector(getLabwareEntities) + + let labwareId = null + if (blowoutLabwareId === SOURCE_WELL_BLOWOUT_DESTINATION) { + labwareId = sourceLabwareId + } else if (blowoutLabwareId === DEST_WELL_BLOWOUT_DESTINATION) { + labwareId = destLabwareId + } + + const labwareZDimension = + labwareId != null + ? labwareEntities[String(labwareId)]?.def.dimensions.zDimension + : 0 + + return ( + <> + {tooltipContent} + {isModalOpen ? ( + setModalOpen(false)} + name={name} + zValue={Number(value)} + updateValue={updateValue} + wellDepthMm={labwareZDimension} + /> + ) : null} + setModalOpen(true)} + id={`BlowoutZOffsetField_${name}`} + data-testid={`BlowoutZOffsetField_${name}`} + > + + + + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/DelayFields.tsx b/protocol-designer/src/components/StepEditForm/fields/DelayFields.tsx index 4a4e05801e4e..dd49dd71d9f8 100644 --- a/protocol-designer/src/components/StepEditForm/fields/DelayFields.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/DelayFields.tsx @@ -37,7 +37,8 @@ export const DelayFields = (props: DelayFieldProps): JSX.Element => { /> {tipPositionFieldName && ( )} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx new file mode 100644 index 000000000000..d1b219b04d8b --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionAllViz.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import round from 'lodash/round' + +import PIPETTE_TIP_IMAGE from '../../../../images/pipette_tip.svg' +import WELL_CROSS_SECTION_IMAGE from '../../../../images/well_cross_section.svg' + +import styles from './TipPositionInput.module.css' + +const WELL_HEIGHT_PIXELS = 145 +const WELL_WIDTH_PIXELS = 100 +const PIXEL_DECIMALS = 2 + +interface TipPositionAllVizProps { + mmFromBottom: number + xPosition: number + wellDepthMm: number + xWidthMm: number +} + +export function TipPositionAllViz(props: TipPositionAllVizProps): JSX.Element { + const { mmFromBottom, xPosition, wellDepthMm, xWidthMm } = props + const fractionOfWellHeight = mmFromBottom / wellDepthMm + const pixelsFromBottom = + Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS + const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) + const bottomPx = wellDepthMm + ? roundedPixelsFromBottom + : mmFromBottom - WELL_HEIGHT_PIXELS + + const xPx = (WELL_WIDTH_PIXELS / xWidthMm) * xPosition + const roundedXPx = round(xPx, PIXEL_DECIMALS) + return ( +
+ + + {props.wellDepthMm !== null && ( + {props.wellDepthMm}mm + )} + +
+ ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css index 181c6ae6f0d9..ef1859083427 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css @@ -4,14 +4,14 @@ display: flex; flex-direction: column; justify-content: space-evenly; - height: 5rem; + height: 4rem; } .main_row { display: flex; flex-direction: row; justify-content: space-between; - margin: 3rem 0 2rem; + margin: 1rem 0 2rem; } .position_from_bottom_input { @@ -56,7 +56,6 @@ font-weight: var(--fw-semibold); color: var(--c-blue); position: absolute; - right: 10px; bottom: 45px; align-self: flex-end; } @@ -65,3 +64,14 @@ position: relative; left: 9px; } + +.tip_position_icon { + height: 1.5rem; + width: 1.5rem; + cursor: pointer; + color: #24313f; /* black80 */ +} + +.tip_position_icon:hover { + background-color: #e6e6e6; +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx index b24178104881..2a303f92c2f6 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionModal.tsx @@ -2,107 +2,87 @@ import * as React from 'react' import { createPortal } from 'react-dom' import cx from 'classnames' import { useTranslation } from 'react-i18next' -import round from 'lodash/round' import { AlertModal, + DIRECTION_COLUMN, Flex, - HandleKeypress, - Icon, InputField, - OutlineButton, RadioGroup, + SPACING, + StyledText, } from '@opentrons/components' import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal' -import modalStyles from '../../../modals/modal.module.css' import { getIsTouchTipField } from '../../../../form-types' -import { TipPositionZAxisViz } from './TipPositionZAxisViz' +import { PDAlert } from '../../../alerts/PDAlert' +import { TOO_MANY_DECIMALS, PERCENT_RANGE_TO_SHOW_WARNING } from './constants' +import { TipPositionAllViz } from './TipPositionAllViz' +import * as utils from './utils' import styles from './TipPositionInput.module.css' -import * as utils from './utils' -import type { StepFieldName } from '../../../../form-types' +import modalStyles from '../../../modals/modal.module.css' -const SMALL_STEP_MM = 1 -const LARGE_STEP_MM = 10 -const DECIMALS_ALLOWED = 1 +import type { StepFieldName } from '../../../../form-types' -interface Props { - closeModal: () => unknown - isIndeterminate?: boolean - mmFromBottom: number | null +type Offset = 'x' | 'y' | 'z' +interface PositionSpec { name: StepFieldName - updateValue: (val: number | null | undefined) => unknown - wellDepthMm: number + value: number | null + updateValue: (val?: number | null) => void } +export type PositionSpecs = Record -const roundValue = (value: number | string | null): number => { - return round(Number(value), DECIMALS_ALLOWED) +interface TipPositionModalProps { + closeModal: () => void + specs: PositionSpecs + wellDepthMm: number + wellXWidthMm: number + wellYWidthMm: number + isIndeterminate?: boolean } -const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' -const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' -type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS - -const getErrorText = (args: { - errors: Error[] - maxMmFromBottom: number - minMmFromBottom: number - isPristine: boolean - t: any -}): string | null => { - const { errors, minMmFromBottom, maxMmFromBottom, isPristine, t } = args - - if (errors.includes(TOO_MANY_DECIMALS)) { - return t('tip_position.errors.TOO_MANY_DECIMALS') - } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { - return t('tip_position.errors.OUT_OF_BOUNDS', { - minMmFromBottom, - maxMmFromBottom, - }) - } else { - return null - } -} +export const TipPositionModal = ( + props: TipPositionModalProps +): JSX.Element | null => { + const { + isIndeterminate, + specs, + wellDepthMm, + wellXWidthMm, + wellYWidthMm, + closeModal, + } = props + const zSpec = specs.z + const ySpec = specs.y + const xSpec = specs.x -const getErrors = (args: { - isDefault: boolean - value: string | null - maxMmFromBottom: number - minMmFromBottom: number -}): Error[] => { - const { isDefault, value, maxMmFromBottom, minMmFromBottom } = args - const errors: Error[] = [] - if (isDefault) return errors - - const v = Number(value) - if (value === null || Number.isNaN(v)) { - // blank or otherwise invalid should show this error as a fallback - return [OUT_OF_BOUNDS] - } - const correctDecimals = round(v, DECIMALS_ALLOWED) === v - const outOfBounds = v > maxMmFromBottom || v < minMmFromBottom + const { t } = useTranslation(['modal', 'button']) - if (!correctDecimals) { - errors.push(TOO_MANY_DECIMALS) + if (zSpec == null || xSpec == null || ySpec == null) { + console.error( + 'expected to find specs for one of the positions but could not' + ) } - if (outOfBounds) { - errors.push(OUT_OF_BOUNDS) - } - return errors -} -export const TipPositionModal = (props: Props): JSX.Element => { - const { isIndeterminate, name, wellDepthMm } = props - const { t } = useTranslation(['modal', 'button']) const defaultMmFromBottom = utils.getDefaultMmFromBottom({ - name, + name: zSpec.name, wellDepthMm, }) - const [value, setValue] = React.useState( - props.mmFromBottom === null ? null : String(props.mmFromBottom) + const [zValue, setZValue] = React.useState( + zSpec?.value == null ? null : String(zSpec?.value) + ) + const [yValue, setYValue] = React.useState( + ySpec?.value == null ? null : String(ySpec?.value) ) + const [xValue, setXValue] = React.useState( + xSpec?.value == null ? null : String(xSpec?.value) + ) + const [isDefault, setIsDefault] = React.useState( - !isIndeterminate && props.mmFromBottom === null + !isIndeterminate && + zSpec.value === null && + ySpec.value === 0 && + xSpec.value === 0 ) // in this modal, pristinity hides the OUT_OF_BOUNDS error only. const [isPristine, setPristine] = React.useState(true) @@ -111,54 +91,83 @@ export const TipPositionModal = (props: Props): JSX.Element => { maxMmFromBottom: number minMmFromBottom: number } => { - if (getIsTouchTipField(name)) { + if (getIsTouchTipField(zSpec?.name ?? '')) { return { - maxMmFromBottom: roundValue(wellDepthMm), - minMmFromBottom: roundValue(wellDepthMm / 2), + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), } } return { - maxMmFromBottom: roundValue(wellDepthMm * 2), + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), minMmFromBottom: 0, } } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() - const errors = getErrors({ - isDefault, - minMmFromBottom, - maxMmFromBottom, - value, - }) - const hasErrors = errors.length > 0 + const { minValue: yMinWidth, maxValue: yMaxWidth } = utils.getMinMaxWidth( + wellYWidthMm + ) + const { minValue: xMinWidth, maxValue: xMaxWidth } = utils.getMinMaxWidth( + wellXWidthMm + ) + + const createErrors = ( + value: string | null, + min: number, + max: number + ): utils.Error[] => { + return utils.getErrors({ isDefault, minMm: min, maxMm: max, value }) + } + const zErrors = createErrors(zValue, minMmFromBottom, maxMmFromBottom) + const xErrors = createErrors(xValue, xMinWidth, xMaxWidth) + const yErrors = createErrors(yValue, yMinWidth, yMaxWidth) + + const hasErrors = + zErrors.length > 0 || xErrors.length > 0 || yErrors.length > 0 const hasVisibleErrors = isPristine - ? errors.includes(TOO_MANY_DECIMALS) + ? zErrors.includes(TOO_MANY_DECIMALS) || + xErrors.includes(TOO_MANY_DECIMALS) || + yErrors.includes(TOO_MANY_DECIMALS) : hasErrors - const errorText = getErrorText({ - errors, - maxMmFromBottom, - minMmFromBottom, - isPristine, - t, - }) + + const createErrorText = ( + errors: utils.Error[], + min: number, + max: number + ): string | null => { + return utils.getErrorText({ errors, minMm: min, maxMm: max, isPristine, t }) + } + + const roundedXMin = utils.roundValue(xMinWidth) + const roundedYMin = utils.roundValue(yMinWidth) + const roundedXMax = utils.roundValue(xMaxWidth) + const roundedYMax = utils.roundValue(yMaxWidth) + + const zErrorText = createErrorText(zErrors, minMmFromBottom, maxMmFromBottom) + const xErrorText = createErrorText(xErrors, roundedXMin, roundedXMax) + const yErrorText = createErrorText(yErrors, roundedYMin, roundedYMax) const handleDone = (): void => { setPristine(false) - if (!hasErrors) { if (isDefault) { - props.updateValue(null) + zSpec?.updateValue(null) + xSpec?.updateValue(0) + ySpec?.updateValue(0) } else { - props.updateValue(value === null ? null : Number(value)) + zSpec?.updateValue(zValue === null ? null : Number(zValue)) + xSpec?.updateValue(xValue === null ? null : Number(xValue)) + ySpec?.updateValue(yValue === null ? null : Number(yValue)) } - props.closeModal() + closeModal() } } const handleCancel = (): void => { - props.closeModal() + closeModal() } - const handleChange = (newValueRaw: string | number): void => { + const handleZChange = (newValueRaw: string | number): void => { // if string, strip non-number characters from string and cast to number const newValue = typeof newValueRaw === 'string' @@ -166,147 +175,196 @@ export const TipPositionModal = (props: Props): JSX.Element => { : String(newValueRaw) if (newValue === '.') { - setValue('0.') + setZValue('0.') } else { - setValue(Number(newValue) >= 0 ? newValue : '0') + setZValue(Number(newValue) >= 0 ? newValue : '0') } } - const handleInputFieldChange = ( + const handleZInputFieldChange = ( e: React.ChangeEvent ): void => { - handleChange(e.currentTarget.value) + handleZChange(e.currentTarget.value) } - const handleIncrementDecrement = (delta: number): void => { - const prevValue = value === null ? defaultMmFromBottom : Number(value) - setIsDefault(false) - handleChange(roundValue(prevValue + delta)) + const handleXChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setXValue('0.') + } else { + setXValue(newValue) + } } - const makeHandleIncrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step) + const handleXInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleXChange(e.currentTarget.value) } - const makeHandleDecrement = (step: number): (() => void) => () => { - handleIncrementDecrement(step * -1) + const handleYChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/g, '') + : String(newValueRaw) + + if (newValue === '.') { + setYValue('0.') + } else { + setYValue(newValue) + } } - const TipPositionInputField = !isDefault && ( - - ) + const handleYInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleYChange(e.currentTarget.value) + } + const isXValueNearEdge = + xValue != null && + (parseInt(xValue) > PERCENT_RANGE_TO_SHOW_WARNING * xMaxWidth || + parseInt(xValue) < PERCENT_RANGE_TO_SHOW_WARNING * xMinWidth) + const isYValueNearEdge = + yValue != null && + (parseInt(yValue) > PERCENT_RANGE_TO_SHOW_WARNING * yMaxWidth || + parseInt(yValue) < PERCENT_RANGE_TO_SHOW_WARNING * yMinWidth) + + const TipPositionInputField = !isDefault ? ( + + + + {t('tip_position.field_titles.x_position')} + + + + + + {t('tip_position.field_titles.y_position')} + + + + + + {t('tip_position.field_titles.z_position')} + + + + + ) : null // Mix Form's asp/disp tip position field has different default value text - const isMixAspDispField = name === 'mix_mmFromBottom' + const isMixAspDispField = zSpec?.name === 'mix_mmFromBottom' return createPortal( - - -
-

{t('tip_position.title')}

-

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

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

{t('tip_position.title')}

+

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

+
+ + {(isXValueNearEdge || isYValueNearEdge) && !isDefault ? ( + + + + ) : null} + +
+ + + ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: isMixAspDispField + ? t('tip_position.radio_button.mix') + : t('tip_position.radio_button.default', { + defaultMmFromBottom, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} -
- - , + +
+ +
+
+
+
, getMainPagePortalEl() ) } diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx index 4b0dc3d512e2..cff1fa05a9af 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx @@ -8,19 +8,23 @@ import styles from './TipPositionInput.module.css' const WELL_HEIGHT_PIXELS = 145 const PIXEL_DECIMALS = 2 -interface Props { - mmFromBottom: number +interface TipPositionZAxisVizProps { wellDepthMm: number + mmFromBottom?: number + mmFromTop?: number } -export const TipPositionZAxisViz = (props: Props): JSX.Element => { - const fractionOfWellHeight = props.mmFromBottom / props.wellDepthMm +export function TipPositionZAxisViz( + props: TipPositionZAxisVizProps +): JSX.Element { + const { mmFromBottom, mmFromTop, wellDepthMm } = props + const positionInTube = mmFromBottom ?? mmFromTop ?? 0 + const fractionOfWellHeight = positionInTube / wellDepthMm const pixelsFromBottom = - Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS - const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS) - const bottomPx = props.wellDepthMm - ? roundedPixelsFromBottom - : props.mmFromBottom - WELL_HEIGHT_PIXELS + fractionOfWellHeight * WELL_HEIGHT_PIXELS - + (mmFromBottom != null ? WELL_HEIGHT_PIXELS : 0) + const bottomPx = round(pixelsFromBottom, PIXEL_DECIMALS) + return (
void + zValue: number | null + name: StepFieldName + updateValue: (val?: number | null) => unknown + wellDepthMm: number + isIndeterminate?: boolean +} + +export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element { + const { + isIndeterminate, + name, + wellDepthMm, + zValue, + closeModal, + updateValue, + } = props + const { t } = useTranslation(['modal', 'button']) + + const isBlowout = name === 'blowout_z_offset' + const defaultMm = isBlowout + ? 0 + : utils.getDefaultMmFromBottom({ + name, + wellDepthMm, + }) + + const [value, setValue] = React.useState( + zValue !== null ? String(zValue) : null + ) + const isSetDefault = isBlowout ? zValue === 0 : zValue === null + const [isDefault, setIsDefault] = React.useState( + !isIndeterminate && isSetDefault + ) + // in this modal, pristinity hides the OUT_OF_BOUNDS error only. + const [isPristine, setPristine] = React.useState(true) + + const getMinMaxMmFromBottom = (): { + maxMmFromBottom: number + minMmFromBottom: number + } => { + if (getIsTouchTipField(name)) { + return { + maxMmFromBottom: utils.roundValue(wellDepthMm), + minMmFromBottom: utils.roundValue(wellDepthMm / 2), + } + } + return { + maxMmFromBottom: utils.roundValue(wellDepthMm * 2), + minMmFromBottom: 0, + } + } + const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom() + + // For blowout from the top of the well + const minFromTop = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const maxFromTop = -wellDepthMm + + const minMm = isBlowout ? maxFromTop : minMmFromBottom + const maxMm = isBlowout ? minFromTop : maxMmFromBottom + + const errors = utils.getErrors({ + isDefault, + minMm, + maxMm, + value, + }) + const hasErrors = errors.length > 0 + const hasVisibleErrors = isPristine + ? errors.includes(TOO_MANY_DECIMALS) + : hasErrors + + const errorText = utils.getErrorText({ + errors, + minMm, + maxMm, + isPristine, + t, + }) + + const handleDone = (): void => { + setPristine(false) + + if (!hasErrors) { + if (isDefault) { + updateValue(null) + } else { + updateValue(value === null ? null : Number(value)) + } + closeModal() + } + } + + const handleCancel = (): void => { + closeModal() + } + + const handleChange = (newValueRaw: string | number): void => { + // if string, strip non-number characters from string and cast to number + const newValue = + typeof newValueRaw === 'string' + ? newValueRaw.replace(/[^-.0-9]/, '') + : String(newValueRaw) + + if (newValue === '.') { + setValue('0.') + } else if (newValue === '-0') { + setValue('0') + } else { + isBlowout + ? setValue(newValue) + : setValue(Number(newValue) >= 0 ? newValue : '0') + } + } + + const handleInputFieldChange = ( + e: React.ChangeEvent + ): void => { + handleChange(e.currentTarget.value) + } + + const handleIncrementDecrement = (delta: number): void => { + const prevValue = value === null ? defaultMm : Number(value) + setIsDefault(false) + handleChange(utils.roundValue(prevValue + delta)) + } + + const makeHandleIncrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step) + } + + const makeHandleDecrement = (step: number): (() => void) => () => { + handleIncrementDecrement(step * -1) + } + + const TipPositionInputField = !isDefault && ( + + ) + + return createPortal( + + +
+

{t('tip_position.title')}

+

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

+
+
+ +
+ ) => { + setIsDefault(e.currentTarget.value === 'default') + }} + options={[ + { + name: isBlowout + ? t('tip_position.radio_button.blowout') + : t('tip_position.radio_button.default', { + defaultMm, + }), + value: 'default', + }, + { + name: t('tip_position.radio_button.custom'), + value: 'custom', + }, + ]} + name="TipPositionOptions" + /> + {TipPositionInputField} +
+ +
+ {!isDefault ? ( +
+ + + + + + +
+ ) : null} + +
+
+
+
+
, + getMainPagePortalEl() + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx new file mode 100644 index 000000000000..36e1d07a0f4a --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionField.test.tsx @@ -0,0 +1,113 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fixture96Plate } from '@opentrons/shared-data' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { getLabwareEntities } from '../../../../../step-forms/selectors' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionField } from '../index' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../../step-forms/selectors') +vi.mock('../ZTipPositionModal') +vi.mock('../TipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} +const mockDelay = 'aspirate_delay_mmFromBottom' +const mockAspirate = 'aspirate_mmFromBottom' +const mockLabwareId = 'mockId' +describe('TipPositionField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + zField: mockDelay, + labwareId: mockLabwareId, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + }, + } + vi.mocked(TipPositionModal).mockReturnValue( +
mock TipPositionModal
+ ) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockLabwareId]: { + id: mockLabwareId, + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + }) + it('renders the input field and header when x and y fields are not provided', () => { + render(props) + screen.getByText('mm') + fireEvent.click(screen.getByRole('textbox', { name: '' })) + expect(screen.getByRole('textbox', { name: '' })).not.toBeDisabled() + screen.getByText('mock ZTipPositionModal') + }) + it('renders the input field but it is disabled', () => { + props = { + ...props, + propsForFields: { + [mockDelay]: { + name: mockDelay, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: true, + } as any, + }, + } + render(props) + expect(screen.getByRole('textbox', { name: '' })).toBeDisabled() + }) + it('renders the icon when x,y, and z fields are provided', () => { + const mockX = 'aspirate_x_position' + const mockY = 'aspirate_y_position' + props = { + zField: mockAspirate, + xField: mockX, + yField: mockY, + labwareId: mockLabwareId, + propsForFields: { + [mockAspirate]: { + name: mockAspirate, + value: null, + updateValue: vi.fn(), + tooltipContent: 'mock content', + isIndeterminate: false, + disabled: false, + } as any, + [mockX]: { + name: mockX, + value: null, + updateValue: vi.fn(), + } as any, + [mockY]: { + name: mockY, + value: null, + updateValue: vi.fn(), + } as any, + }, + } + render(props) + fireEvent.click(screen.getByTestId('TipPositionIcon_aspirate_mmFromBottom')) + screen.getByText('mock TipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx new file mode 100644 index 000000000000..6054bd2eb2d8 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/TipPositionModal.test.tsx @@ -0,0 +1,140 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { TipPositionModal } from '../TipPositionModal' +import { TipPositionAllViz } from '../TipPositionAllViz' + +vi.mock('../TipPositionAllViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockUpdateZSpec = vi.fn() +const mockUpdateXSpec = vi.fn() +const mockUpdateYSpec = vi.fn() + +describe('TipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + wellDepthMm: 50, + wellXWidthMm: 10.3, + wellYWidthMm: 10.5, + isIndeterminate: false, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: null, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: 0, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 0, + updateValue: mockUpdateYSpec, + }, + }, + } + vi.mocked(TipPositionAllViz).mockReturnValue(
mock TipPositionViz
) + }) + it('renders the modal text and radio button text', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot aspirates') + screen.getByRole('radio', { name: '1 mm from the bottom center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders the alert if the x/y position values are too close to the max/min for x value', () => { + props.specs.x.value = 9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) + it('renders the alert if the x/y position values are too close to the max/min for y value', () => { + props.specs.y.value = -9.7 + render(props) + screen.getByText('warning') + screen.getByText( + 'The X and/or Y position value is close to edge of the well and might collide with it' + ) + }) + it('renders the custom options, captions, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(3) + screen.getByText('X position') + screen.getByText('between -5.1 and 5.2') + screen.getByText('Y position') + screen.getByText('between -5.2 and 5.3') + screen.getByText('Z position') + screen.getByText('between 0 and 100') + screen.getByText('mock TipPositionViz') + }) + it('renders a custom input field and clicks on it, calling the mock updates', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3 } }) + const yInputField = screen.getAllByRole('textbox', { name: '' })[1] + fireEvent.change(yInputField, { target: { value: -2 } }) + const zInputField = screen.getAllByRole('textbox', { name: '' })[2] + fireEvent.change(zInputField, { target: { value: 10 } }) + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(mockUpdateXSpec).toHaveBeenCalled() + expect(mockUpdateYSpec).toHaveBeenCalled() + expect(mockUpdateZSpec).toHaveBeenCalled() + }) + it('renders custom input fields and displays error texts', () => { + props = { + ...props, + specs: { + z: { + name: 'aspirate_mmFromBottom', + value: 101, + updateValue: mockUpdateZSpec, + }, + y: { + name: 'aspirate_y_position', + value: -500, + updateValue: mockUpdateXSpec, + }, + x: { + name: 'aspirate_x_position', + value: 10.7, + updateValue: mockUpdateYSpec, + }, + }, + } + render(props) + fireEvent.click(screen.getByText('done')) + // display out of bounds error + screen.getByText('accepted range is 0 to 100') + screen.getByText('accepted range is -5.2 to 5.3') + screen.getByText('accepted range is -5.1 to 5.2') + const xInputField = screen.getAllByRole('textbox', { name: '' })[0] + fireEvent.change(xInputField, { target: { value: 3.55555 } }) + fireEvent.click(screen.getByText('done')) + // display too many decimals error + screen.getByText('a max of 1 decimal place is allowed') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx new file mode 100644 index 000000000000..015d5437dbb7 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/__tests__/ZTipPositionModal.test.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderWithProviders } from '../../../../../__testing-utils__' +import { i18n } from '../../../../../localization' +import { ZTipPositionModal } from '../ZTipPositionModal' +import { TipPositionZAxisViz } from '../TipPositionZAxisViz' + +vi.mock('../TipPositionZAxisViz') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ZTipPositionModal', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + closeModal: vi.fn(), + zValue: -2, + updateValue: vi.fn(), + wellDepthMm: 30, + name: 'blowout_z_offset', + } + vi.mocked(TipPositionZAxisViz).mockReturnValue( +
mock TipPositionZAxisViz
+ ) + }) + it('renders the text and radio buttons', () => { + render(props) + screen.getByText('Tip Positioning') + screen.getByText('Change from where in the well the robot emits blowout') + screen.getByRole('radio', { name: '0 mm from the top center (default)' }) + screen.getByRole('radio', { name: 'Custom' }) + fireEvent.click(screen.getByText('cancel')) + expect(props.closeModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('done')) + expect(props.closeModal).toHaveBeenCalled() + expect(props.updateValue).toHaveBeenCalled() + }) + it('renders the custom option, caption, and visual', () => { + render(props) + fireEvent.click(screen.getByRole('radio', { name: 'Custom' })) + expect(screen.getAllByRole('textbox', { name: '' })).toHaveLength(1) + screen.getByText('between -30 and 0') + screen.getByText('mock TipPositionZAxisViz') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts new file mode 100644 index 000000000000..528d9a0262e8 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/constants.ts @@ -0,0 +1,5 @@ +export const DECIMALS_ALLOWED = 1 +export const SMALL_STEP_MM = 1 +export const LARGE_STEP_MM = 10 +export const TOO_MANY_DECIMALS: 'TOO_MANY_DECIMALS' = 'TOO_MANY_DECIMALS' +export const PERCENT_RANGE_TO_SHOW_WARNING = 0.9 diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx index ccaa80e13d58..5f60d13cd792 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/index.tsx @@ -2,13 +2,16 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { + COLORS, + Flex, FormGroup, + Icon, InputField, Tooltip, useHoverTooltip, UseHoverTooltipTargetProps, } from '@opentrons/components' -import { getWellsDepth } from '@opentrons/shared-data' +import { getWellsDepth, getWellDimension } from '@opentrons/shared-data' import { getIsTouchTipField, getIsDelayPositionField, @@ -16,28 +19,40 @@ import { import { selectors as stepFormSelectors } from '../../../../step-forms' import { TipPositionModal } from './TipPositionModal' import { getDefaultMmFromBottom } from './utils' +import { ZTipPositionModal } from './ZTipPositionModal' +import type { + TipXOffsetFields, + TipYOffsetFields, + TipZOffsetFields, +} from '../../../../form-types' +import type { FieldPropsByName } from '../../types' +import type { PositionSpecs } from './TipPositionModal' + import stepFormStyles from '../../StepEditForm.module.css' import styles from './TipPositionInput.module.css' -import type { FieldProps } from '../../types' -interface TipPositionFieldProps extends FieldProps { +interface TipPositionFieldProps { + propsForFields: FieldPropsByName + zField: TipZOffsetFields + xField?: TipXOffsetFields + yField?: TipYOffsetFields labwareId?: string | null - className?: string } export function TipPositionField(props: TipPositionFieldProps): JSX.Element { + const { labwareId, propsForFields, zField, xField, yField } = props const { - disabled, - name, + name: zName, + value: rawZValue, + updateValue: zUpdateValue, tooltipContent, - updateValue, isIndeterminate, - labwareId, - value: rawValue, - } = props + disabled, + } = propsForFields[zField] + const { t } = useTranslation('application') const [targetProps, tooltipProps] = useHoverTooltip() - const [isModalOpen, setModalOpen] = React.useState(false) + const [isModalOpen, setModalOpen] = React.useState(false) const labwareEntities = useSelector(stepFormSelectors.getLabwareEntities) const labwareDef = labwareId != null && labwareEntities[labwareId] != null @@ -45,68 +60,137 @@ export function TipPositionField(props: TipPositionFieldProps): JSX.Element { : null let wellDepthMm = 0 + let wellXWidthMm = 0 + let wellYWidthMm = 0 + if (labwareDef != null) { - // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths + // NOTE: only taking depth of first well in labware def, UI not currently equipped for multiple depths/widths const firstWell = labwareDef.wells.A1 if (firstWell) { wellDepthMm = getWellsDepth(labwareDef, ['A1']) + wellXWidthMm = getWellDimension(labwareDef, ['A1'], 'x') + wellYWidthMm = getWellDimension(labwareDef, ['A1'], 'y') } } - if (wellDepthMm === 0 && labwareId != null && labwareDef != null) { + if ( + (wellDepthMm === 0 || wellXWidthMm === 0 || wellYWidthMm === 0) && + labwareId != null && + labwareDef != null + ) { console.error( - `expected to find the well depth mm with labwareId ${labwareId} but could not` + `expected to find all well dimensions mm with labwareId ${labwareId} but could not` ) } - const handleOpen = (): void => { - if (wellDepthMm) { + const handleOpen = (has3Specs: boolean): void => { + if (has3Specs && wellDepthMm && wellXWidthMm && wellYWidthMm) { + setModalOpen(true) + } + if (!has3Specs && wellDepthMm) { setModalOpen(true) } } const handleClose = (): void => { setModalOpen(false) } - const isTouchTipField = getIsTouchTipField(name) - const isDelayPositionField = getIsDelayPositionField(name) - let value: string | number = '0' - const mmFromBottom = typeof rawValue === 'number' ? rawValue : null + const isTouchTipField = getIsTouchTipField(zName) + const isDelayPositionField = getIsDelayPositionField(zName) + let zValue: string | number = '0' + const mmFromBottom = typeof rawZValue === 'number' ? rawZValue : null if (wellDepthMm !== null) { // show default value for field in parens if no mmFromBottom value is selected - value = - mmFromBottom !== null - ? mmFromBottom - : getDefaultMmFromBottom({ name, wellDepthMm }) + zValue = + mmFromBottom ?? getDefaultMmFromBottom({ name: zName, wellDepthMm }) } + + let modal = ( + + ) + if (yField != null && xField != null) { + const { + name: xName, + value: rawXValue, + updateValue: xUpdateValue, + } = propsForFields[xField] + const { + name: yName, + value: rawYValue, + updateValue: yUpdateValue, + } = propsForFields[yField] + + const specs: PositionSpecs = { + z: { + name: zName, + value: mmFromBottom, + updateValue: zUpdateValue, + }, + x: { + name: xName, + value: rawXValue != null ? Number(rawXValue) : null, + updateValue: xUpdateValue, + }, + y: { + name: yName, + value: rawYValue != null ? Number(rawYValue) : null, + updateValue: yUpdateValue, + }, + } + + modal = ( + + ) + } + return ( <> {tooltipContent} - {isModalOpen && ( - - )} + {isModalOpen ? modal : null} - + {yField != null && xField != null ? ( + handleOpen(true) : () => {}} + id={`TipPositionIcon_${zName}`} + data-testid={`TipPositionIcon_${zName}`} + width="5rem" + > + + + ) : ( + handleOpen(false)} + value={String(zValue)} + isIndeterminate={isIndeterminate} + units={t('units.millimeter')} + id={`TipPositionField_${zName}`} + /> + )} ) diff --git a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts index c4d4590c5dc8..96ed4729d495 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts +++ b/protocol-designer/src/components/StepEditForm/fields/TipPositionField/utils.ts @@ -1,9 +1,13 @@ +import round from 'lodash/round' +import { getIsTouchTipField } from '../../../../form-types' import { DEFAULT_MM_FROM_BOTTOM_ASPIRATE, DEFAULT_MM_FROM_BOTTOM_DISPENSE, DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP, } from '../../../../constants' -import { StepFieldName, getIsTouchTipField } from '../../../../form-types' +import { DECIMALS_ALLOWED, TOO_MANY_DECIMALS } from './constants' +import type { StepFieldName } from '../../../../form-types' + // TODO: Ian + Brian 2019-02-13 this should switch on stepType, not use field // name to infer step type! // @@ -41,3 +45,70 @@ export function getDefaultMmFromBottom(args: { return DEFAULT_MM_TOUCH_TIP_OFFSET_FROM_TOP + wellDepthMm } } + +export const roundValue = (value: number | string | null): number => { + return value === null ? 0 : round(Number(value), DECIMALS_ALLOWED) +} + +const OUT_OF_BOUNDS: 'OUT_OF_BOUNDS' = 'OUT_OF_BOUNDS' +export type Error = typeof TOO_MANY_DECIMALS | typeof OUT_OF_BOUNDS + +export const getErrorText = (args: { + errors: Error[] + maxMm: number + minMm: number + isPristine: boolean + t: any +}): string | null => { + const { errors, minMm, maxMm, isPristine, t } = args + + if (errors.includes(TOO_MANY_DECIMALS)) { + return t('tip_position.errors.TOO_MANY_DECIMALS') + } else if (!isPristine && errors.includes(OUT_OF_BOUNDS)) { + return t('tip_position.errors.OUT_OF_BOUNDS', { + minMm, + maxMm, + }) + } else { + return null + } +} + +export const getErrors = (args: { + isDefault: boolean + value: string | null + maxMm: number + minMm: number +}): Error[] => { + const { isDefault, value: rawValue, maxMm, minMm } = args + const errors: Error[] = [] + if (isDefault) return errors + + const value = Number(rawValue) + if (rawValue === null || Number.isNaN(value)) { + // blank or otherwise invalid should show this error as a fallback + return [OUT_OF_BOUNDS] + } + const incorrectDecimals = round(value, DECIMALS_ALLOWED) !== value + const outOfBounds = value > maxMm || value < minMm + + if (incorrectDecimals) { + errors.push(TOO_MANY_DECIMALS) + } + if (outOfBounds) { + errors.push(OUT_OF_BOUNDS) + } + return errors +} + +interface MinMaxValues { + minValue: number + maxValue: number +} + +export const getMinMaxWidth = (width: number): MinMaxValues => { + return { + minValue: -width * 0.5, + maxValue: width * 0.5, + } +} diff --git a/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx b/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx index a9dceb482a29..464b15b4f7f5 100644 --- a/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/TiprackField.tsx @@ -1,32 +1,62 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { FormGroup, DropdownField } from '@opentrons/components' +import { + FormGroup, + DropdownField, + useHoverTooltip, + Tooltip, + Box, +} from '@opentrons/components' import { selectors as uiLabwareSelectors } from '../../../ui/labware' -import styles from '../StepEditForm.module.css' - +import { getPipetteEntities } from '../../../step-forms/selectors' import type { FieldProps } from '../types' -export function TiprackField(props: FieldProps): JSX.Element { - const { name, value, onFieldBlur, onFieldFocus, updateValue } = props - const { t } = useTranslation('form') +import styles from '../StepEditForm.module.css' + +interface TiprackFieldProps extends FieldProps { + pipetteId?: unknown +} +export function TiprackField(props: TiprackFieldProps): JSX.Element { + const { + name, + value, + onFieldBlur, + onFieldFocus, + updateValue, + pipetteId, + } = props + const { t } = useTranslation(['form', 'tooltip']) + const [targetProps, tooltipProps] = useHoverTooltip() + const pipetteEntities = useSelector(getPipetteEntities) const options = useSelector(uiLabwareSelectors.getTiprackOptions) + const defaultTipracks = + pipetteId != null ? pipetteEntities[pipetteId as string].tiprackDefURI : [] + const pipetteOptions = options.filter(option => + defaultTipracks.includes(option.defURI) + ) + const hasMissingTiprack = defaultTipracks.length > pipetteOptions.length return ( - - ) => { - updateValue(e.currentTarget.value) - }} - /> - + + + ) => { + updateValue(e.currentTarget.value) + }} + /> + + {hasMissingTiprack ? ( + {t('tooltip:missing_tiprack')} + ) : null} + ) } diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx new file mode 100644 index 000000000000..fec53a25ac4e --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/BlowoutZOffsetField.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { fixture96Plate } from '@opentrons/shared-data' +import { SOURCE_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' +import { getLabwareEntities } from '../../../../step-forms/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { ZTipPositionModal } from '../TipPositionField/ZTipPositionModal' +import { BlowoutZOffsetField } from '../BlowoutZOffsetField' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../../../step-forms/selectors') +vi.mock('../TipPositionField/ZTipPositionModal') +const render = (props: React.ComponentProps) => { + return renderWithProviders()[0] +} +const mockSourceId = 'sourceId' +describe('BlowoutZOffsetField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + disabled: false, + value: null, + name: 'blowout_z_offset', + updateValue: vi.fn(), + onFieldBlur: vi.fn(), + onFieldFocus: vi.fn(), + destLabwareId: SOURCE_WELL_BLOWOUT_DESTINATION, + sourceLabwareId: mockSourceId, + blowoutLabwareId: 'blowoutId', + } + vi.mocked(getLabwareEntities).mockReturnValue({ + [mockSourceId]: { + id: 'mockLabwareId', + labwareDefURI: 'mock uri', + def: fixture96Plate as LabwareDefinition2, + }, + }) + vi.mocked(ZTipPositionModal).mockReturnValue( +
mock ZTipPositionModal
+ ) + }) + it('renders the input field', () => { + render(props) + screen.getByTestId('BlowoutZOffsetField_blowout_z_offset') + }) + it('renders the modal when input field is clicked on', () => { + render(props) + fireEvent.click(screen.getByTestId('BlowoutZOffsetField_blowout_z_offset')) + screen.getByText('mock ZTipPositionModal') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx new file mode 100644 index 000000000000..979155a4d889 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/__tests__/TiprackField.test.tsx @@ -0,0 +1,60 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { screen } from '@testing-library/react' +import { i18n } from '../../../../localization' +import { getPipetteEntities } from '../../../../step-forms/selectors' +import { renderWithProviders } from '../../../../__testing-utils__' +import { getTiprackOptions } from '../../../../ui/labware/selectors' +import { TiprackField } from '../TiprackField' + +vi.mock('../../../../ui/labware/selectors') +vi.mock('../../../../step-forms/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} +const mockMockId = 'mockId' +describe('TiprackField', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + disabled: false, + value: null, + name: 'tipRackt', + updateValue: vi.fn(), + onFieldBlur: vi.fn(), + onFieldFocus: vi.fn(), + pipetteId: mockMockId, + } + vi.mocked(getPipetteEntities).mockReturnValue({ + [mockMockId]: { + name: 'p50_single_flex', + spec: {} as any, + id: mockMockId, + tiprackLabwareDef: [], + tiprackDefURI: ['mockDefURI1', 'mockDefURI2'], + }, + }) + vi.mocked(getTiprackOptions).mockReturnValue([ + { + value: 'mockValue', + name: 'tiprack1', + defURI: 'mockDefURI1', + }, + { + value: 'mockValue', + name: 'tiprack2', + defURI: 'mockDefURI2', + }, + ]) + }) + it('renders the dropdown field and texts', () => { + render(props) + screen.getByText('tip rack') + screen.getByText('tiprack1') + screen.getByText('tiprack2') + }) +}) diff --git a/protocol-designer/src/components/StepEditForm/fields/index.ts b/protocol-designer/src/components/StepEditForm/fields/index.ts index 15d7f4bb21f4..70d10ffa616d 100644 --- a/protocol-designer/src/components/StepEditForm/fields/index.ts +++ b/protocol-designer/src/components/StepEditForm/fields/index.ts @@ -7,6 +7,7 @@ export { TextField } from './TextField' /* Specialized Fields */ export { BlowoutLocationField } from './BlowoutLocationField' +export { BlowoutZOffsetField } from './BlowoutZOffsetField' export { ChangeTipField } from './ChangeTipField' export { DelayFields } from './DelayFields' export { DisposalVolumeField } from './DisposalVolumeField' diff --git a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx index 8873c10eb527..1976767e7e52 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MagnetForm.tsx @@ -1,27 +1,31 @@ import * as React from 'react' -import cx from 'classnames' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { FormGroup } from '@opentrons/components' import { MAGNETIC_MODULE_V1 } from '@opentrons/shared-data' import { selectors as uiModuleSelectors } from '../../../ui/modules' -import { selectors as stepFormSelectors } from '../../../step-forms' -import { maskField } from '../../../steplist/fieldLevel' +import { getModuleEntities } from '../../../step-forms/selectors' +import { + MAX_ENGAGE_HEIGHT_V1, + MAX_ENGAGE_HEIGHT_V2, + MIN_ENGAGE_HEIGHT_V1, + MIN_ENGAGE_HEIGHT_V2, +} from '../../../constants' import { TextField, RadioGroupField } from '../fields' -import styles from '../StepEditForm.module.css' +import type { StepFormProps } from '../types' -import { StepFormProps } from '../types' +import styles from '../StepEditForm.module.css' -export const MagnetForm = (props: StepFormProps): JSX.Element => { +export function MagnetForm(props: StepFormProps): JSX.Element { const moduleLabwareOptions = useSelector( uiModuleSelectors.getMagneticLabwareOptions ) + const moduleEntities = useSelector(getModuleEntities) const { t } = useTranslation(['application', 'form']) + const { propsForFields, formData } = props + const { magnetAction, moduleId } = formData - const moduleEntities = useSelector(stepFormSelectors.getModuleEntities) - const { magnetAction, moduleId } = props.formData - const moduleModel = moduleId ? moduleEntities[moduleId]?.model : null - + const moduleModel = moduleEntities[moduleId].model const moduleOption: string | null | undefined = moduleLabwareOptions[0] ? moduleLabwareOptions[0].name : 'No magnetic module' @@ -29,12 +33,21 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { const defaultEngageHeight = useSelector( uiModuleSelectors.getMagnetLabwareEngageHeight ) - - const engageHeightCaption = defaultEngageHeight - ? `Recommended: ${String(maskField('engageHeight', defaultEngageHeight))}` - : null - - const { propsForFields } = props + const engageHeightMinMax = + moduleModel === MAGNETIC_MODULE_V1 + ? t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V1, + high: MAX_ENGAGE_HEIGHT_V1, + }) + : t('magnet_height_caption', { + low: MIN_ENGAGE_HEIGHT_V2, + high: MAX_ENGAGE_HEIGHT_V2, + }) + const engageHeightDefault = + defaultEngageHeight != null + ? t('magnet_recommended', { default: defaultEngageHeight }) + : '' + const engageHeightCaption = `${engageHeightMinMax} ${engageHeightDefault}` return (
@@ -91,18 +104,6 @@ export const MagnetForm = (props: StepFormProps): JSX.Element => { )}
- {magnetAction === 'engage' && ( -
-
-
- )}
) } diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 87cfdbcd49b2..f0eb043b0814 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -17,6 +17,7 @@ import { VolumeField, WellOrderField, WellSelectionField, + BlowoutZOffsetField, } from '../fields' import { TiprackField } from '../fields/TiprackField' import { @@ -51,7 +52,10 @@ export const MixForm = (props: StepFormProps): JSX.Element => {
- + {is96Channel ? ( ) : null} @@ -117,7 +121,10 @@ export const MixForm = (props: StepFormProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { label={t('form:step_edit_form.field.touchTip.label')} > { stepType: formData.stepType, })} /> +
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx index 77eaa424f36c..eadd4fad2a90 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestFields.tsx @@ -11,6 +11,7 @@ import { TextField, TipPositionField, WellOrderField, + BlowoutZOffsetField, } from '../../fields' import { MixFields } from '../../fields/MixFields' import { @@ -95,7 +96,10 @@ export const SourceDestFields = (props: SourceDestFieldsProps): JSX.Element => { tiprack={propsForFields.tipRack.value} /> { className={styles.small_field} > { stepType: formData.stepType, })} /> + )} {
- + {is96Channel ? ( ) : null} diff --git a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx index c14b358dc0c8..bcd35a1636fd 100644 --- a/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/TemperatureForm.tsx @@ -4,16 +4,17 @@ import { useTranslation } from 'react-i18next' import { FormGroup } from '@opentrons/components' import { selectors as uiModuleSelectors } from '../../../ui/modules' import { StepFormDropdown, RadioGroupField, TextField } from '../fields' -import styles from '../StepEditForm.module.css' import type { StepFormProps } from '../types' -export const TemperatureForm = (props: StepFormProps): JSX.Element => { +import styles from '../StepEditForm.module.css' + +export function TemperatureForm(props: StepFormProps): JSX.Element { const { t } = useTranslation(['application', 'form']) const moduleLabwareOptions = useSelector( uiModuleSelectors.getTemperatureLabwareOptions ) - const temperatureModuleId = useSelector( - uiModuleSelectors.getSingleTemperatureModuleId + const temperatureModuleIds = useSelector( + uiModuleSelectors.getTemperatureModuleIds ) const { propsForFields } = props @@ -36,56 +37,47 @@ export const TemperatureForm = (props: StepFormProps): JSX.Element => { options={moduleLabwareOptions} /> - {/* TODO (ka 2020-1-6): - moduleID dropdown will autoselect when creating a new step, - but this will not be the case when returning to a never saved form. - Rather than defaulting to one or the other when null, - display a message (copy, design, etc TBD) that you need to select a module to continue - */} - - {moduleId === null && ( -

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

- )} - {moduleId === temperatureModuleId && temperatureModuleId != null && ( - <> -
- - {setTemperature === 'true' && ( - - )} -
-
- -
- - )} + {temperatureModuleIds != null + ? temperatureModuleIds.map(id => + id === moduleId ? ( + +
+ + {setTemperature === 'true' && ( + + )} +
+
+ +
+
+ ) : null + ) + : null}
) diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx index 6ddefc3af74d..dbc5bb5a4080 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/HeaterShakerForm.test.tsx @@ -31,7 +31,7 @@ vi.mock('../../fields', async importOriginal => { const render = (props: React.ComponentProps) => { return renderWithProviders(, { - i18nInstance: i18n as any, + i18nInstance: i18n, })[0] } diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx index 736294018a9d..34146989405c 100644 --- a/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/MagnetForm.test.tsx @@ -1,5 +1,98 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, afterEach, vi, beforeEach } from 'vitest' +import '@testing-library/jest-dom/vitest' +import { cleanup, fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { getMagneticLabwareOptions } from '../../../../ui/modules/selectors' +import { getModuleEntities } from '../../../../step-forms/selectors' +import { getMagnetLabwareEngageHeight } from '../../../../ui/modules/utils' +import { MagnetForm } from '../MagnetForm' + +vi.mock('../../../../ui/modules/utils') +vi.mock('../../../../ui/modules/selectors') +vi.mock('../../../../step-forms/selectors') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} describe('MagnetForm', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'magnet', + stepType: 'magnet', + moduleId: 'magnetId', + magnetAction: 'engage', + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + magnetAction: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'magnetAction', + updateValue: vi.fn(), + value: 'engage', + }, + engageHeight: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'engage height', + updateValue: vi.fn(), + value: 10, + }, + }, + } + vi.mocked(getMagneticLabwareOptions).mockReturnValue([ + { name: 'mock name', value: 'mockValue' }, + ]) + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV2', + type: 'magneticModuleType', + }, + }) + vi.mocked(getMagnetLabwareEngageHeight).mockReturnValue(null) + }) + afterEach(() => { + vi.restoreAllMocks() + cleanup() + }) + + it('renders the text and radio buttons for v2', () => { + render(props) + screen.getByText('magnet') + screen.getByText('module') + screen.getByText('mock name') + screen.getByText('Magnet action') + const engage = screen.getByText('Engage') + screen.getByText('Disengage') + fireEvent.click(engage) + screen.getByText('Must be between -2.5 to 25.') + }) + it('renders the input text for v1', () => { + vi.mocked(getModuleEntities).mockReturnValue({ + magnetId: { + id: 'magnetId', + model: 'magneticModuleV1', + type: 'magneticModuleType', + }, + }) + render(props) + screen.getByText('Must be between 0 to 45.') + }) }) diff --git a/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx new file mode 100644 index 000000000000..a32894d3b84d --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/forms/__tests__/TemperatureForm.test.tsx @@ -0,0 +1,95 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { + getTemperatureLabwareOptions, + getTemperatureModuleIds, +} from '../../../../ui/modules/selectors' +import { TemperatureForm } from '../TemperatureForm' + +vi.mock('../../../../ui/modules/selectors', async importOriginal => { + const actualFields = await importOriginal< + typeof import('../../../../ui/modules/selectors') + >() + return { + ...actualFields, + getTemperatureLabwareOptions: vi.fn(), + getTemperatureModuleIds: vi.fn(), + } +}) +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('TemperatureForm', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + formData: { + id: 'formId', + stepType: 'temperature', + moduleId: 'mockId', + setTemperature: true, + } as any, + focusHandlers: { + blur: vi.fn(), + focus: vi.fn(), + dirtyFields: [], + focusedField: null, + }, + propsForFields: { + moduleId: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: 'mockId', + }, + setTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'setTemperature', + updateValue: vi.fn(), + value: true, + }, + targetTemperature: { + onFieldFocus: vi.fn(), + onFieldBlur: vi.fn(), + errorToShow: null, + disabled: false, + name: 'targetTemperature', + updateValue: vi.fn(), + value: null, + }, + }, + } + + vi.mocked(getTemperatureModuleIds).mockReturnValue(['mockId']) + vi.mocked(getTemperatureLabwareOptions).mockReturnValue([ + { + name: 'mock module', + value: 'mockId', + }, + ]) + }) + + it('renders a temperature module', () => { + render(props) + screen.getByText('temperature') + screen.getByText('module') + const change = screen.getByText('Change to temperature') + screen.getByText('Deactivate module') + fireEvent.click(change) + const changeTempInput = screen.getByRole('combobox', { name: '' }) + fireEvent.change(changeTempInput, { target: { value: 40 } }) + }) +}) diff --git a/protocol-designer/src/components/__tests__/EditModules.test.tsx b/protocol-designer/src/components/__tests__/EditModules.test.tsx index 2cb2ed8c55f4..fb183a3e9e69 100644 --- a/protocol-designer/src/components/__tests__/EditModules.test.tsx +++ b/protocol-designer/src/components/__tests__/EditModules.test.tsx @@ -1,19 +1,29 @@ import * as React from 'react' import { screen } from '@testing-library/react' import { vi, beforeEach, describe, it } from 'vitest' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + TEMPERATURE_MODULE_TYPE, +} from '@opentrons/shared-data' import { i18n } from '../../localization' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getDismissedHints } from '../../tutorial/selectors' import { EditModules } from '../EditModules' import { EditModulesModal } from '../modals/EditModulesModal' import { renderWithProviders } from '../../__testing-utils__' +import { getEnableMoam } from '../../feature-flags/selectors' +import { getRobotType } from '../../file-data/selectors' +import { EditMultipleModulesModal } from '../modals/EditModulesModal/EditMultipleModulesModal' import type { HintKey } from '../../tutorial' vi.mock('../../step-forms/selectors') +vi.mock('../modals/EditModulesModal/EditMultipleModulesModal') vi.mock('../modals/EditModulesModal') vi.mock('../../tutorial/selectors') - +vi.mock('../../file-data/selectors') +vi.mock('../../feature-flags/selectors') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -51,11 +61,22 @@ describe('EditModules', () => { vi.mocked(EditModulesModal).mockReturnValue(
mock EditModulesModal
) + vi.mocked(EditMultipleModulesModal).mockReturnValue( +
mock EditMultipleModulesModal
+ ) vi.mocked(getDismissedHints).mockReturnValue([hintKey]) + vi.mocked(getRobotType).mockReturnValue(OT2_ROBOT_TYPE) + vi.mocked(getEnableMoam).mockReturnValue(true) }) - it('renders the edit modules modal', () => { + it('renders the edit modules modal for single modules', () => { render(props) screen.getByText('mock EditModulesModal') }) + it('renders multiple edit modules modal', () => { + props.moduleToEdit.moduleType = TEMPERATURE_MODULE_TYPE + vi.mocked(getRobotType).mockReturnValue(FLEX_ROBOT_TYPE) + render(props) + screen.getByText('mock EditMultipleModulesModal') + }) }) diff --git a/protocol-designer/src/components/alerts/Alerts.tsx b/protocol-designer/src/components/alerts/Alerts.tsx index 6d5f191486a5..1fc95e8162fb 100644 --- a/protocol-designer/src/components/alerts/Alerts.tsx +++ b/protocol-designer/src/components/alerts/Alerts.tsx @@ -11,6 +11,7 @@ import { import { selectors as stepFormSelectors } from '../../step-forms' import { StepFieldName } from '../../steplist/fieldLevel' import { selectors as fileDataSelectors } from '../../file-data' +import { PRESAVED_STEP_ID } from '../../steplist' import { getVisibleFormWarnings, getVisibleFormErrors, @@ -105,16 +106,6 @@ const AlertsComponent = (props: Props): JSX.Element => { }) } - const dismissWarning = (dismissId: string): void => { - if (stepId) { - dispatch( - dismissActions.dismissTimelineWarning({ - type: dismissId, - stepId, - }) - ) - } - } const makeHandleCloseWarning = (dismissId?: string | null) => () => { console.assert( dismissId, @@ -153,6 +144,28 @@ const AlertsComponent = (props: Props): JSX.Element => { dismissId: warning.type, })) + const dismissWarning = (dismissId: string): void => { + const isTimelineWarning = Object.values(timelineWarnings).some( + warning => warning.dismissId === dismissId + ) + if (isTimelineWarning && stepId) { + dispatch( + dismissActions.dismissTimelineWarning({ + type: dismissId, + stepId, + }) + ) + } else { + dispatch( + dismissActions.dismissFormWarning({ + type: dismissId, + // if stepId does not exist, assume it is a presaved step + stepId: stepId ?? PRESAVED_STEP_ID, + }) + ) + } + } + return ( <> {componentType === 'Form' diff --git a/protocol-designer/src/components/lists/TitledStepList.tsx b/protocol-designer/src/components/lists/TitledStepList.tsx index 0e3da16c5428..1b88b291b1ec 100644 --- a/protocol-designer/src/components/lists/TitledStepList.tsx +++ b/protocol-designer/src/components/lists/TitledStepList.tsx @@ -110,7 +110,12 @@ export function TitledStepList(props: Props): JSX.Element { )} {iconName && ( - + )}

{props.title}

{collapsible && ( diff --git a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx index aab430bf549d..b10c6d75407e 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx @@ -265,5 +265,39 @@ export const useAnnouncements = (): Announcement[] => { ), }, + { + announcementKey: 'customParamsAndMultiTipAndModule8.1', + image: , + heading: t('announcements.header', { pd: PD }), + message: ( + <> +

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

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

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

+ + ), + }, ] } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 97266b07252f..76b97572b47d 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -15,48 +15,39 @@ import { TYPOGRAPHY, useHoverTooltip, Tooltip, + DIRECTION_COLUMN, + Box, + StyledText, } from '@opentrons/components' import type { StyleProps } from '@opentrons/components' import type { RobotType } from '@opentrons/shared-data' -const EQUIPMENT_OPTION_STYLE = css` - background-color: ${COLORS.white}; - border-radius: ${BORDERS.borderRadius8}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - +const ARROW_STYLE = css` + color: ${COLORS.grey50}; + cursor: pointer; &:hover { - background-color: ${COLORS.grey10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey35}; - } - - &:focus { - outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; - outline-offset: 3px; + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_SELECTED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - +const ARROW_STYLE_ACTIVE = css` + color: ${COLORS.blue50}; + cursor: pointer; &:hover { - background-color: ${COLORS.blue10}; - border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + color: ${COLORS.black80}; } ` -const EQUIPMENT_OPTION_DISABLED_STYLE = css` - ${EQUIPMENT_OPTION_STYLE} - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - - &:hover { - background-color: ${COLORS.white}; - border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; - } +const ARROW_STYLE_DISABLED = css` + color: ${COLORS.grey50}; ` + +interface MultiplesProps { + numItems: number + maxItems: number + setValue: (num: number) => void + isDisabled: boolean +} interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler isSelected: boolean @@ -65,6 +56,7 @@ interface EquipmentOptionProps extends StyleProps { image?: React.ReactNode showCheckbox?: boolean disabled?: boolean + multiples?: MultiplesProps } export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { const { @@ -75,10 +67,51 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { showCheckbox = false, disabled = false, robotType, + multiples, ...styleProps } = props - const { t } = useTranslation('tooltip') - const [targetProps, tooltipProps] = useHoverTooltip() + const { t } = useTranslation(['tooltip', 'shared']) + const [equipmentTargetProps, equipmentTooltipProps] = useHoverTooltip() + const [tempTargetProps, tempTooltipProps] = useHoverTooltip() + const [numMultiples, setNumMultiples] = React.useState(0) + + const EQUIPMENT_OPTION_STYLE = css` + background-color: ${COLORS.white}; + border-radius: ${BORDERS.borderRadius8}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + background-color: ${multiples ? COLORS.white : COLORS.grey10}; + border: 1px ${BORDERS.styleSolid} + ${multiples ? COLORS.grey30 : COLORS.grey35}; + } + + &:focus { + outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; + outline-offset: 3px; + } + ` + + const EQUIPMENT_OPTION_SELECTED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.blue10}; + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2); + } + ` + + const EQUIPMENT_OPTION_DISABLED_STYLE = css` + ${EQUIPMENT_OPTION_STYLE} + background-color: ${COLORS.white}; + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + + &:hover { + border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; + } + ` let equipmentOptionStyle if (disabled) { @@ -102,6 +135,66 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { ) } else if (showCheckbox && disabled) { iconInfo = + } else if (multiples != null) { + const { numItems, maxItems, isDisabled } = multiples + let upArrowStyle = ARROW_STYLE + if (isDisabled || numItems === maxItems) { + upArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + upArrowStyle = ARROW_STYLE_ACTIVE + } + let downArrowStyle = ARROW_STYLE + if (numItems === 0) { + downArrowStyle = ARROW_STYLE_DISABLED + } else if (numItems > 0) { + downArrowStyle = ARROW_STYLE_ACTIVE + } + + iconInfo = ( + + { + multiples.setValue(numMultiples + 1) + setNumMultiples(prevNumMultiples => prevNumMultiples + 1) + } + } + > + + + { + multiples.setValue(numMultiples - 1) + setNumMultiples(prevNumMultiples => prevNumMultiples - 1) + } + } + > + + + {isDisabled || numMultiples === 7 ? ( + + {t('not_enough_space_for_temp')} + + ) : null} + + ) } return ( @@ -117,31 +210,52 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { : BORDERS.lineBorder } borderRadius={BORDERS.borderRadius8} - cursor={disabled ? 'auto' : 'pointer'} + cursor={disabled || multiples != null ? 'auto' : 'pointer'} backgroundColor={disabled ? COLORS.grey30 : COLORS.transparent} onClick={disabled ? undefined : onClick} {...styleProps} - {...targetProps} + {...equipmentTargetProps} css={equipmentOptionStyle} > {iconInfo} {image} - - {text} - + + + {text} + + {multiples != null ? ( + <> + + + {t('shared:amount')} + {multiples.numItems} + + + ) : null} + {disabled ? ( - + {t( robotType === FLEX_ROBOT_TYPE ? 'disabled_no_space_additional_items' diff --git a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx index 1140109b3037..63a7903907ef 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/InputField.tsx @@ -8,7 +8,6 @@ import { COLORS, DIRECTION_COLUMN, Flex, - RESPONSIVENESS, SPACING, TYPOGRAPHY, DISPLAY_INLINE_BLOCK, @@ -60,10 +59,6 @@ function Input(props: InputFieldProps): JSX.Element { border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey30}; font-size: ${TYPOGRAPHY.fontSizeP}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - padding: 0; - } - &:active { border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index 492b408ae5f8..bcebf6313c3e 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -30,11 +30,14 @@ import { getModuleDisplayName, getModuleType, FLEX_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, + MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' import { getIsCrashablePipetteSelected } from '../../../step-forms' import gripperImage from '../../../images/flex_gripper.png' import wasteChuteImage from '../../../images/waste_chute.png' import trashBinImage from '../../../images/flex_trash_bin.png' +import { getEnableMoam } from '../../../feature-flags/selectors' import { uuid } from '../../../utils' import { selectors as featureFlagSelectors } from '../../../feature-flags' import { CrashInfoBox, ModuleDiagram } from '../../modules' @@ -42,7 +45,8 @@ import { ModuleFields } from '../FilePipettesModal/ModuleFields' import { GoBack } from './GoBack' import { getCrashableModuleSelected, - getLastCheckedEquipment, + getDisabledEquipment, + getNextAvailableModuleSlot, getTrashBinOptionDisabled, } from './utils' import { EquipmentOption } from './EquipmentOption' @@ -50,6 +54,8 @@ import { HandleEnter } from './HandleEnter' import type { AdditionalEquipment, WizardTileProps } from './types' +const MAX_TEMPERATURE_MODULES = 7 + export const DEFAULT_SLOT_MAP: { [moduleModel in ModuleModel]?: string } = { [THERMOCYCLER_MODULE_V2]: 'B1', [HEATERSHAKER_MODULE_V1]: 'D1', @@ -186,13 +192,14 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { function FlexModuleFields(props: WizardTileProps): JSX.Element { const { watch, setValue } = props + const enableMoamFf = useSelector(getEnableMoam) const modules = watch('modules') const additionalEquipment = watch('additionalEquipment') const moduleTypesOnDeck = modules != null ? Object.values(modules).map(module => module.type) : [] const trashBinDisabled = getTrashBinOptionDisabled({ additionalEquipment, - moduleTypesOnDeck, + modules, }) const handleSetEquipmentOption = (equipment: AdditionalEquipment): void => { @@ -214,6 +221,82 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { {FLEX_SUPPORTED_MODULE_MODELS.map(moduleModel => { const moduleType = getModuleType(moduleModel) const moduleOnDeck = moduleTypesOnDeck.includes(moduleType) + + let defaultSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + defaultSlot = 'B1' + } else if (moduleType === MAGNETIC_BLOCK_TYPE) { + defaultSlot = 'D2' + } + const isDisabled = getDisabledEquipment({ + additionalEquipment, + modules, + })?.includes(moduleType) + const handleMultiplesClick = (num: number): void => { + const temperatureModules = + modules != null + ? Object.entries(modules).filter( + ([key, module]) => module.type === TEMPERATURE_MODULE_TYPE + ) + : [] + + if (num > temperatureModules.length) { + for (let i = 0; i < num - temperatureModules.length; i++) { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: getNextAvailableModuleSlot( + modules, + additionalEquipment + ), + }, + }) + } + } else if (num < temperatureModules.length) { + const modulesToRemove = temperatureModules.length - num + for (let i = 0; i < modulesToRemove; i++) { + const lastTempKey = + temperatureModules[temperatureModules.length - 1 - i][0] + // @ts-expect-error: TS can't determine modules's type correctly + const { [lastTempKey]: omit, ...rest } = modules + setValue('modules', rest) + } + } + } + + const handleOnClick = (): void => { + if ( + (moduleType !== TEMPERATURE_MODULE_TYPE && enableMoamFf) || + !enableMoamFf + ) { + if (moduleOnDeck) { + const updatedModules = + modules != null + ? Object.fromEntries( + Object.entries(modules).filter( + ([key, value]) => value.type !== moduleType + ) + ) + : {} + setValue('modules', updatedModules) + } else { + setValue('modules', { + ...modules, + [uuid()]: { + model: moduleModel, + type: moduleType, + slot: defaultSlot, + }, + }) + } + } + } + return ( } text={getModuleDisplayName(moduleModel)} - disabled={ - getLastCheckedEquipment({ - additionalEquipment, - moduleTypesOnDeck, - }) === moduleType + disabled={isDisabled && !moduleOnDeck} + onClick={handleOnClick} + multiples={ + moduleType === TEMPERATURE_MODULE_TYPE && enableMoamFf + ? { + maxItems: MAX_TEMPERATURE_MODULES, + setValue: handleMultiplesClick, + numItems: + modules != null + ? Object.values(modules).filter( + module => module.type === TEMPERATURE_MODULE_TYPE + ).length + : 0, + isDisabled: isDisabled ?? false, + } + : undefined + } + showCheckbox={ + enableMoamFf ? moduleType !== TEMPERATURE_MODULE_TYPE : true } - onClick={() => { - if (moduleOnDeck) { - const updatedModulesByType = - modules != null - ? Object.fromEntries( - Object.entries(modules).filter( - ([key, value]) => value.type !== moduleType - ) - ) - : {} - setValue('modules', updatedModulesByType) - } else { - setValue('modules', { - ...modules, - [uuid()]: { - model: moduleModel, - type: moduleType, - slot: DEFAULT_SLOT_MAP[moduleModel] ?? '', - }, - }) - } - }} - showCheckbox /> ) })} @@ -271,6 +345,11 @@ function FlexModuleFields(props: WizardTileProps): JSX.Element { robotType={FLEX_ROBOT_TYPE} onClick={() => handleSetEquipmentOption('wasteChute')} isSelected={additionalEquipment.includes('wasteChute')} + disabled={ + modules != null + ? Object.values(modules).some(module => module.slot === 'D3') + : false + } image={ ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }) +) + export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const { getValues, goBack, proceed, setValue, watch } = props const { t } = useTranslation(['modal', 'application']) const { fields, pipettesByMount } = getValues() const additionalEquipment = watch('additionalEquipment') + const modules = watch('modules') const isOt2 = fields.robotType === OT2_ROBOT_TYPE const stagingAreaItems = additionalEquipment.filter(equipment => // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId // and a cutoutFixtureId so that we don't have to string parse here to generate them equipment.includes('stagingArea') ) + const unoccupiedStagingAreaSlots = getUnoccupiedStagingAreaSlots(modules) const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( item => { @@ -49,14 +59,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { } ) - const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( - cutoutId => ({ - cutoutId, - cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, - }) - ) - - STANDARD_EMPTY_SLOTS.forEach(emptySlot => { + unoccupiedStagingAreaSlots.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( ({ cutoutId }) => cutoutId === emptySlot.cutoutId @@ -67,7 +70,9 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { }) const initialSlots = - stagingAreaItems.length > 0 ? savedStagingAreaSlots : STANDARD_EMPTY_SLOTS + stagingAreaItems.length > 0 + ? savedStagingAreaSlots + : unoccupiedStagingAreaSlots const [updatedSlots, setUpdatedSlots] = React.useState( initialSlots diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx index 091283611359..c83b1e994047 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/EquipmentOption.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' -import { screen, cleanup } from '@testing-library/react' +import { screen, cleanup, fireEvent } from '@testing-library/react' import { BORDERS, COLORS } from '@opentrons/components' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { i18n } from '../../../../localization' @@ -39,7 +39,7 @@ describe('EquipmentOption', () => { } render(props) expect(screen.getByLabelText('EquipmentOption_flex_mockText')).toHaveStyle( - `background-color: ${COLORS.white}` + `background-color: ${COLORS.grey10}` ) }) it('renders the equipment option without check not selected and image', () => { @@ -73,4 +73,21 @@ describe('EquipmentOption', () => { `border: ${BORDERS.activeLineBorder}` ) }) + it('renders the equipment option with multiples allowed', () => { + props = { + ...props, + multiples: { + numItems: 1, + maxItems: 4, + setValue: vi.fn(), + isDisabled: false, + }, + } + render(props) + screen.getByText('Amount:') + screen.getByText('1') + fireEvent.click(screen.getByTestId('EquipmentOption_upArrow')) + expect(props.multiples?.setValue).toHaveBeenCalled() + screen.getByTestId('EquipmentOption_downArrow') + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index ba9924ee13ea..63da7f3ed30a 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -5,7 +5,10 @@ import { fireEvent, screen, cleanup } from '@testing-library/react' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders } from '../../../../__testing-utils__' import { i18n } from '../../../../localization' -import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' +import { + getDisableModuleRestrictions, + getEnableMoam, +} from '../../../../feature-flags/selectors' import { CrashInfoBox } from '../../../modules' import { ModuleFields } from '../../FilePipettesModal/ModuleFields' import { ModulesAndOtherTile } from '../ModulesAndOtherTile' @@ -58,6 +61,7 @@ describe('ModulesAndOtherTile', () => { ...props, ...mockWizardTileProps, } as WizardTileProps + vi.mocked(getEnableMoam).mockReturnValue(true) vi.mocked(CrashInfoBox).mockReturnValue(
mock CrashInfoBox
) vi.mocked(EquipmentOption).mockReturnValue(
mock EquipmentOption
) vi.mocked(getDisableModuleRestrictions).mockReturnValue(false) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index 213f3466c0e8..240120c8b923 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -1,17 +1,22 @@ +import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_TYPE, + SINGLE_RIGHT_SLOT_FIXTURE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { it, describe, expect } from 'vitest' import { FLEX_TRASH_DEFAULT_SLOT, - getLastCheckedEquipment, + getUnoccupiedStagingAreaSlots, getTrashSlot, + getNextAvailableModuleSlot, + getDisabledEquipment, + getTrashBinOptionDisabled, } from '../utils' +import { STANDARD_EMPTY_SLOTS } from '../StagingAreaTile' import type { FormPipettesByMount } from '../../../../step-forms' -import type { AdditionalEquipment, FormState } from '../types' +import type { FormState } from '../types' let MOCK_FORM_STATE = { fields: { @@ -28,43 +33,169 @@ let MOCK_FORM_STATE = { additionalEquipment: [], } as FormState -describe('getLastCheckedEquipment', () => { - it('should return null when there is no trash bin', () => { - const result = getLastCheckedEquipment({ - additionalEquipment: [], - moduleTypesOnDeck: [], +describe('getUnoccupiedStagingAreaSlots', () => { + it('should return all staging area slots when there are no modules', () => { + const result = getUnoccupiedStagingAreaSlots(null) + expect(result).toStrictEqual(STANDARD_EMPTY_SLOTS) + }) + it('should return one staging area slot when there are modules in the way of the other slots', () => { + const result = getUnoccupiedStagingAreaSlots({ + 0: { model: 'magneticBlockV1', type: 'magneticBlockType', slot: 'A3' }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B3', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C3', + }, }) - expect(result).toBe(null) + expect(result).toStrictEqual([ + { cutoutId: 'cutoutD3', cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE }, + ]) }) - it('should return null if not all the modules or staging areas are selected', () => { - const LastCheckedProps = { - additionalEquipment: [ + describe('getNextAvailableModuleSlot', () => { + it('should return D1 when there are no modules or staging areas', () => { + const result = getNextAvailableModuleSlot(null, []) + expect(result).toStrictEqual('D1') + }) + it('should return a C3 when all the modules are on the deck', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'magneticBlockV1', + type: 'magneticBlockType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + }, + [] + ) + expect(result).toStrictEqual('C3') + }) + }) + it('should return an empty string when all the modules and staging area slots are on the deck without TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) + it('should return an empty string when all the modules and staging area slots are on the deck with TC', () => { + const result = getNextAvailableModuleSlot( + { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'thermocyclerModuleV2', + type: 'thermocyclerModuleType', + slot: 'B1', + }, + }, + [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(null) + 'trashBin', + ] + ) + expect(result).toStrictEqual('') + }) +}) +describe('getNextAvailableModuleSlot', () => { + it('should return nothing as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: null, + }) + expect(result).toStrictEqual([]) }) - it('should return temperature module if other modules and staging areas are selected', () => { - const LastCheckedProps = { + it('should return the TC as disabled', () => { + const result = getDisabledEquipment({ + additionalEquipment: [], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toStrictEqual([THERMOCYCLER_MODULE_TYPE]) + }) + it('should return all module types if there is no available slot', () => { + const result = getDisabledEquipment({ additionalEquipment: [ - 'trashBin', 'stagingArea_cutoutA3', 'stagingArea_cutoutB3', 'stagingArea_cutoutC3', 'stagingArea_cutoutD3', - ] as AdditionalEquipment[], - moduleTypesOnDeck: [THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE], - } - const result = getLastCheckedEquipment(LastCheckedProps) - expect(result).toBe(TEMPERATURE_MODULE_TYPE) + 'trashBin', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + }, + }) + expect(result).toStrictEqual([ + THERMOCYCLER_MODULE_TYPE, + TEMPERATURE_MODULE_TYPE, + HEATERSHAKER_MODULE_TYPE, + ]) }) }) - describe('getTrashSlot', () => { - it('should return the default slot A3 when there is no staging area in that slot', () => { + it('should return the default slot A3 when there is no staging area or module in that slot', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['trashBin'], @@ -72,7 +203,7 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe(FLEX_TRASH_DEFAULT_SLOT) }) - it('should return cutoutB3 when there is a staging area in slot A3', () => { + it('should return cutoutA1 when there is a staging area in slot A3', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, additionalEquipment: ['stagingArea_cutoutA3'], @@ -80,4 +211,59 @@ describe('getTrashSlot', () => { const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe('cutoutA1') }) + describe('getTrashBinOptionDisabled', () => { + it('returns false when there is a trash bin already', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + }, + }) + expect(result).toBe(false) + }) + it('returns false when there is an available slot', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: ['trashBin'], + modules: null, + }) + expect(result).toBe(false) + }) + it('returns true when there is no available slot and trash bin is not selected yet', () => { + const result = getTrashBinOptionDisabled({ + additionalEquipment: [ + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', + ], + modules: { + 0: { + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + slot: 'D1', + }, + 1: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'C1', + }, + 2: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'B1', + }, + 3: { + model: 'temperatureModuleV2', + type: 'temperatureModuleType', + slot: 'A1', + }, + }, + }) + expect(result).toBe(true) + }) + }) }) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index eea2264199a3..b19ab426f65f 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -240,15 +240,15 @@ export function CreateFileWizard(): JSX.Element | null { const newTiprackModels: string[] = uniq( pipettes.flatMap(pipette => pipette.tiprackDefURI) ) + const FLEX_MIDDLE_SLOTS = ['C2', 'B2', 'A2'] + const OT2_MIDDLE_SLOTS = ['2', '5', '8', '11'] newTiprackModels.forEach((tiprackDefURI, index) => { - const ot2Slots = index === 0 ? '2' : '5' - const flexSlots = index === 0 ? 'C2' : 'B2' dispatch( labwareIngredActions.createContainer({ slot: values.fields.robotType === FLEX_ROBOT_TYPE - ? flexSlots - : ot2Slots, + ? FLEX_MIDDLE_SLOTS[index] + : OT2_MIDDLE_SLOTS[index], labwareDefURI: tiprackDefURI, adapterUnderLabwareDefURI: values.pipettesByMount.left.pipetteName === 'p1000_96' diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 989dabe2839a..20abcf27cb3d 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -1,62 +1,52 @@ import { - getModuleType, HEATERSHAKER_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { isModuleWithCollisionIssue } from '../../modules' -import { - FLEX_SUPPORTED_MODULE_MODELS, - DEFAULT_SLOT_MAP, -} from './ModulesAndOtherTile' +import { STANDARD_EMPTY_SLOTS } from './StagingAreaTile' -import type { ModuleType } from '@opentrons/shared-data' +import type { DeckConfiguration, ModuleType } from '@opentrons/shared-data' import type { FormModules } from '../../../step-forms' import type { AdditionalEquipment, FormState } from './types' export const FLEX_TRASH_DEFAULT_SLOT = 'cutoutA3' -const ALL_STAGING_AREAS = 4 - -interface LastCheckedProps { - additionalEquipment: AdditionalEquipment[] - moduleTypesOnDeck: ModuleType[] -} -export const getLastCheckedEquipment = ( - props: LastCheckedProps -): string | null => { - const { additionalEquipment, moduleTypesOnDeck } = props - const hasAllStagingAreas = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - const hasTrashBin = additionalEquipment.includes('trashBin') - if (!hasTrashBin || !hasAllStagingAreas) { - return null - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) - ) { - return TEMPERATURE_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return THERMOCYCLER_MODULE_TYPE - } - - if ( - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) - ) { - return HEATERSHAKER_MODULE_TYPE - } - - return null -} +const MODULES_SLOTS_FLEX = [ + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, +] export const getCrashableModuleSelected = ( modules: FormModules | null, @@ -75,29 +65,15 @@ export const getCrashableModuleSelected = ( return crashableModuleOnDeck } -export const getTrashBinOptionDisabled = (props: LastCheckedProps): boolean => { - const { additionalEquipment, moduleTypesOnDeck } = props - const allStagingAreasInUse = - additionalEquipment.filter(equipment => equipment.includes('stagingArea')) - .length === ALL_STAGING_AREAS - - const allModulesInSideSlotsOnDeck = - moduleTypesOnDeck.includes(HEATERSHAKER_MODULE_TYPE) && - moduleTypesOnDeck.includes(TEMPERATURE_MODULE_TYPE) && - moduleTypesOnDeck.includes(THERMOCYCLER_MODULE_TYPE) - - return allStagingAreasInUse && allModulesInSideSlotsOnDeck -} - export const MOVABLE_TRASH_CUTOUTS = [ - { - value: 'cutoutA1', - slot: 'A1', - }, { value: 'cutoutA3', slot: 'A3', }, + { + value: 'cutoutA1', + slot: 'A1', + }, { value: 'cutoutB1', slot: 'B1', @@ -124,37 +100,152 @@ export const MOVABLE_TRASH_CUTOUTS = [ }, ] +export const getUnoccupiedStagingAreaSlots = ( + modules: FormState['modules'] +): DeckConfiguration => { + let unoccupiedSlots = STANDARD_EMPTY_SLOTS + const moduleCutoutIds = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [`cutout${module.slot}`, 'cutoutA1'] + : `cutout${module.slot}` + ) + : [] + + unoccupiedSlots = unoccupiedSlots.filter(emptySlot => { + return !moduleCutoutIds.includes(emptySlot.cutoutId) + }) + + return unoccupiedSlots +} + +export const getNextAvailableModuleSlot = ( + modules: FormState['modules'], + additionalEquipment: FormState['additionalEquipment'] +): string => { + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + const stagingAreas = additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + const stagingAreaCutouts = stagingAreas.map(cutout => cutout.split('_')[1]) + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') + ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] + const trashBin = additionalEquipment.find(equipment => + equipment.includes('trashBin') + ) + const hasTC = + modules != null + ? Object.values(modules).some( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + : false + + // removing slot(s) for the trash if spaces are limited + let removeSlotForTrash = MODULES_SLOTS_FLEX + if (trashBin != null && hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -2) + } else if (trashBin != null && !hasTC) { + removeSlotForTrash = MODULES_SLOTS_FLEX.slice(0, -1) + } + const unoccupiedSlot = removeSlotForTrash.find( + cutout => + !stagingAreaCutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) + ) + if (unoccupiedSlot == null) { + return '' + } + + return unoccupiedSlot?.slot ?? '' +} + +interface DisabledEquipmentProps { + additionalEquipment: AdditionalEquipment[] + modules: FormModules | null +} + +export const getDisabledEquipment = ( + props: DisabledEquipmentProps +): string[] => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const disabledEquipment: string[] = [] + + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] + + if (moduleSlots.includes('A1') || moduleSlots.includes('B1')) { + disabledEquipment.push(THERMOCYCLER_MODULE_TYPE) + } + if (nextAvailableSlot === '') { + disabledEquipment.push(TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE) + } + + return disabledEquipment +} + +export const getTrashBinOptionDisabled = ( + props: DisabledEquipmentProps +): boolean => { + const { additionalEquipment, modules } = props + const nextAvailableSlot = getNextAvailableModuleSlot( + modules, + additionalEquipment + ) + const hasTrashBinAlready = additionalEquipment.includes('trashBin') + return nextAvailableSlot === '' && !hasTrashBinAlready +} + export const getTrashSlot = (values: FormState): string => { const { additionalEquipment, modules } = values - const moduleTypesOnDeck = - modules != null ? Object.values(modules).map(module => module.type) : [] + const moduleSlots = + modules != null + ? Object.values(modules).flatMap(module => + module.type === THERMOCYCLER_MODULE_TYPE + ? [module.slot, 'A1'] + : module.slot + ) + : [] const stagingAreas = additionalEquipment.filter(equipment => equipment.includes('stagingArea') ) // TODO(Jr, 11/16/23): refactor additionalEquipment to store cutouts // so the split isn't needed const cutouts = stagingAreas.map(cutout => cutout.split('_')[1]) - - if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) { - return FLEX_TRASH_DEFAULT_SLOT - } - - const moduleSlots: string[] = FLEX_SUPPORTED_MODULE_MODELS.reduce( - (slots: string[], model) => { - const moduleType = getModuleType(model) - if (moduleTypesOnDeck.includes(moduleType)) { - const slot = String(DEFAULT_SLOT_MAP[model]) - return moduleType === THERMOCYCLER_MODULE_TYPE - ? [...slots, 'A1', slot] - : [...slots, slot] - } - return slots - }, - [] + const hasWasteChute = additionalEquipment.find(equipment => + equipment.includes('wasteChute') ) + const wasteChuteSlot = Boolean(hasWasteChute) + ? [WASTE_CHUTE_CUTOUT as string] + : [] + const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( cutout => - !cutouts.includes(cutout.value) && !moduleSlots.includes(cutout.slot) + !cutouts.includes(cutout.value) && + !moduleSlots.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value) ) if (unoccupiedSlot == null) { console.error( diff --git a/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx b/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx new file mode 100644 index 000000000000..cc31c4eb0718 --- /dev/null +++ b/protocol-designer/src/components/modals/EditModulesModal/EditMultipleModulesModal.tsx @@ -0,0 +1,274 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector, useDispatch } from 'react-redux' +import { Controller, useForm, useWatch } from 'react-hook-form' +import { + BUTTON_TYPE_SUBMIT, + OutlineButton, + ModalShell, + Flex, + SPACING, + DIRECTION_ROW, + Box, + Text, + ALIGN_CENTER, + JUSTIFY_FLEX_END, + JUSTIFY_END, + DeckConfigurator, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { + DeckConfiguration, + SINGLE_RIGHT_SLOT_FIXTURE, + TEMPERATURE_MODULE_CUTOUTS, + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, + TEMPERATURE_MODULE_V2_FIXTURE, +} from '@opentrons/shared-data' +import { createModule, deleteModule } from '../../../step-forms/actions' +import { getLabwareOnSlot, getSlotIsEmpty } from '../../../step-forms' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { getLabwareIsCompatible } from '../../../utils/labwareModuleCompatibility' +import { PDAlert } from '../../alerts/PDAlert' +import type { Control, ControllerRenderProps } from 'react-hook-form' +import type { CutoutId, ModuleType } from '@opentrons/shared-data' +import type { ModuleOnDeck } from '../../../step-forms' + +export interface EditMultipleModulesModalValues { + selectedAddressableAreas: string[] +} + +interface EditMultipleModulesModalComponentProps + extends EditMultipleModulesModalProps { + control: Control + moduleLocations: string[] | null +} + +const EditMultipleModulesModalComponent = ( + props: EditMultipleModulesModalComponentProps +): JSX.Element => { + const { t } = useTranslation(['button', 'alert']) + const { + onCloseClick, + allModulesOnDeck, + control, + moduleLocations, + moduleType, + } = props + const initialDeckSetup = useSelector(getInitialDeckSetup) + + const selectedSlots = useWatch({ + control, + name: 'selectedAddressableAreas', + defaultValue: moduleLocations ?? [], + }) + const occupiedCutoutIds = selectedSlots + .map(slot => { + const hasModSlot = + allModulesOnDeck.find( + module => + module.type === moduleType && slot === `cutout${module.slot}` + ) != null + const labwareOnSlot = getLabwareOnSlot(initialDeckSetup, slot) + const isLabwareCompatible = + (labwareOnSlot && + getLabwareIsCompatible(labwareOnSlot.def, moduleType)) ?? + true + const isEmpty = + (getSlotIsEmpty(initialDeckSetup, slot, true) || hasModSlot) && + isLabwareCompatible + + return { slot, isEmpty } + }) + .filter(slot => !slot.isEmpty) + const hasConflictedSlot = occupiedCutoutIds.length > 0 + const mappedModules: DeckConfiguration = + moduleLocations != null + ? moduleLocations.flatMap(location => { + return [ + { + cutoutId: location as CutoutId, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + }, + ] + }) + : [] + const STANDARD_EMPTY_SLOTS: DeckConfiguration = TEMPERATURE_MODULE_CUTOUTS.map( + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }) + ) + + STANDARD_EMPTY_SLOTS.forEach(emptySlot => { + if ( + !mappedModules.some(({ cutoutId }) => cutoutId === emptySlot.cutoutId) + ) { + mappedModules.push(emptySlot) + } + }) + + const selectableSlots = + mappedModules.length > 0 ? mappedModules : STANDARD_EMPTY_SLOTS + const [updatedSlots, setUpdatedSlots] = React.useState( + selectableSlots + ) + const handleClickAdd = ( + cutoutId: string, + field: ControllerRenderProps< + EditMultipleModulesModalValues, + 'selectedAddressableAreas' + > + ): void => { + const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { + if (slot.cutoutId === cutoutId) { + return { + ...slot, + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + } + } + return slot + }) + setUpdatedSlots(modifiedSlots) + const updatedSelectedSlots = [...selectedSlots, cutoutId] + field.onChange(updatedSelectedSlots) + } + + const handleClickRemove = ( + cutoutId: string, + field: ControllerRenderProps< + EditMultipleModulesModalValues, + 'selectedAddressableAreas' + > + ): void => { + const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { + if (slot.cutoutId === cutoutId) { + return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE } + } + return slot + }) + setUpdatedSlots(modifiedSlots) + + field.onChange(selectedSlots.filter(item => item !== cutoutId)) + } + const occupiedSlots = occupiedCutoutIds.map( + occupiedCutout => occupiedCutout.slot.split('cutout')[1] + ) + const alertDescription = t( + `alert:module_placement.SLOTS_OCCUPIED.${ + occupiedSlots.length === 1 ? 'single' : 'multi' + }`, + { + slotName: occupiedSlots, + } + ) + + return ( + <> + + + + {hasConflictedSlot ? ( + + ) : null} + + + ( + handleClickAdd(cutoutId, field)} + handleClickRemove={cutoutId => handleClickRemove(cutoutId, field)} + showExpansion={false} + /> + )} + /> + + + {t('cancel')} + + {t('save')} + + + + ) +} + +export interface EditMultipleModulesModalProps { + onCloseClick: () => void + allModulesOnDeck: ModuleOnDeck[] + moduleType: ModuleType +} +export function EditMultipleModulesModal( + props: EditMultipleModulesModalProps +): JSX.Element { + const { onCloseClick, allModulesOnDeck, moduleType } = props + const { t } = useTranslation('modules') + const dispatch = useDispatch() + const { control, handleSubmit } = useForm() + const moduleLocations = Object.values(allModulesOnDeck) + .filter(module => module.type === moduleType) + .map(temp => `cutout${temp.slot}`) + + const onSaveClick = (data: EditMultipleModulesModalValues): void => { + onCloseClick() + + data.selectedAddressableAreas.forEach(aa => { + const moduleInSlot = Object.values(allModulesOnDeck).find(module => + aa.includes(module.slot) + ) + if (!moduleInSlot) { + dispatch( + createModule({ + slot: aa.split('cutout')[1], + type: TEMPERATURE_MODULE_TYPE, + model: TEMPERATURE_MODULE_V2, + }) + ) + } + }) + Object.values(allModulesOnDeck).forEach(module => { + const moduleCutout = `cutout${module.slot}` + if (!data.selectedAddressableAreas.includes(moduleCutout)) { + dispatch(deleteModule(module.id)) + } + }) + } + + return ( +
+ + + + {t('module_display_names.multipleTemperatureModuleTypes')} + + + + +
+ ) +} diff --git a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx new file mode 100644 index 000000000000..fa01bd44ecff --- /dev/null +++ b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditMultipleModulesModal.test.tsx @@ -0,0 +1,106 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { fireEvent, screen, cleanup } from '@testing-library/react' +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../localization' +import { getInitialDeckSetup } from '../../../../step-forms/selectors' +import { getLabwareIsCompatible } from '../../../../utils/labwareModuleCompatibility' +import { + getLabwareOnSlot, + getSlotIsEmpty, + ModuleOnDeck, +} from '../../../../step-forms' +import { EditMultipleModulesModal } from '../EditMultipleModulesModal' +import type * as Components from '@opentrons/components' + +vi.mock('../../../../step-forms/selectors') +vi.mock('../../../../utils/labwareModuleCompatibility') +vi.mock('../../../../step-forms') +vi.mock('@opentrons/components', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + DeckConfigurator: vi.fn(() =>
mock deck config
), + } +}) + +const render = ( + props: React.ComponentProps +) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockTemp: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, +} +const mockTemp2: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'A1', + moduleState: {} as any, +} +const mockHS: ModuleOnDeck = { + id: 'heaterShakerId', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + moduleState: {} as any, + slot: 'A1', +} +describe('EditMultipleModulesModal', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + moduleType: 'temperatureModuleType', + onCloseClick: vi.fn(), + allModulesOnDeck: [mockTemp, mockTemp2], + } + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + temperatureId: mockTemp, + temperatureId2: mockTemp2, + }, + labware: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + vi.mocked(getLabwareOnSlot).mockReturnValue(null) + vi.mocked(getSlotIsEmpty).mockReturnValue(true) + }) + afterEach(() => { + cleanup() + }) + it('renders modal and buttons with no error', () => { + vi.mocked(getLabwareIsCompatible).mockReturnValue(true) + render(props) + screen.getByText('mock deck config') + screen.getByText('Multiple Temperatures') + fireEvent.click(screen.getByRole('button', { name: 'cancel' })) + expect(props.onCloseClick).toHaveBeenCalled() + screen.getByRole('button', { name: 'save' }) + }) + it('renders modal with a cannot place module error', () => { + vi.mocked(getLabwareOnSlot).mockReturnValue({ slot: 'A1' } as any) + vi.mocked(getLabwareIsCompatible).mockReturnValue(false) + vi.mocked(getSlotIsEmpty).mockReturnValue(false) + props.allModulesOnDeck = [mockTemp, mockTemp2, mockHS] + vi.mocked(getInitialDeckSetup).mockReturnValue({ + modules: { + heaterShakerId: mockHS, + }, + labware: {}, + additionalEquipmentOnDeck: {}, + pipettes: {}, + }) + render(props) + screen.getByText('warning') + screen.getByText('Cannot place module') + screen.getByText('Multiple slots are occupied') + }) +}) diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 40df5ef14a27..896463c295c5 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -27,10 +27,12 @@ import { CrashInfoBox } from './CrashInfoBox' import { ModuleRow } from './ModuleRow' import { AdditionalItemsRow } from './AdditionalItemsRow' import { isModuleWithCollisionIssue } from './utils' -import styles from './styles.module.css' -import { AdditionalEquipmentEntity } from '@opentrons/step-generation' import { StagingAreasRow } from './StagingAreasRow' +import { MultipleModuleRow } from './MultipleModuleRow' + +import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' +import styles from './styles.module.css' export interface Props { modules: ModulesForEditModulesCard openEditModuleModal: (moduleType: ModuleType, moduleId?: string) => void @@ -38,6 +40,7 @@ export interface Props { export function EditModulesCard(props: Props): JSX.Element { const { modules, openEditModuleModal } = props + const pipettesByMount = useSelector( stepFormSelectors.getPipettesForEditPipetteForm ) @@ -67,10 +70,10 @@ export function EditModulesCard(props: Props): JSX.Element { ) const hasCrashableMagneticModule = magneticModuleOnDeck && - isModuleWithCollisionIssue(magneticModuleOnDeck.model) + isModuleWithCollisionIssue(magneticModuleOnDeck[0].model) const hasCrashableTempModule = temperatureModuleOnDeck && - isModuleWithCollisionIssue(temperatureModuleOnDeck.model) + isModuleWithCollisionIssue(temperatureModuleOnDeck[0].model) const isHeaterShakerOnDeck = Boolean(heaterShakerOnDeck) const showTempPipetteCollisons = @@ -130,22 +133,33 @@ export function EditModulesCard(props: Props): JSX.Element { ) : null} {SUPPORTED_MODULE_TYPES_FILTERED.map((moduleType, i) => { const moduleData = modules[moduleType] - if (moduleData) { + if (moduleData != null && moduleData.length === 1) { return ( ) + } else if (moduleData != null && moduleData.length > 1) { + return ( + + ) } else { return ( ) diff --git a/protocol-designer/src/components/modules/MultipleModuleRow.tsx b/protocol-designer/src/components/modules/MultipleModuleRow.tsx new file mode 100644 index 000000000000..b38978d7dfcd --- /dev/null +++ b/protocol-designer/src/components/modules/MultipleModuleRow.tsx @@ -0,0 +1,121 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { + LabeledValue, + OutlineButton, + ModuleIcon, + C_DARK_GRAY, + SPACING, +} from '@opentrons/components' +import { actions as stepFormActions } from '../../step-forms' +import { DEFAULT_MODEL_FOR_MODULE_TYPE } from '../../constants' +import { ModuleDiagram } from './ModuleDiagram' +import { FlexSlotMap } from './FlexSlotMap' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' +import type { ModuleOnDeck } from '../../step-forms' + +import styles from './styles.module.css' + +interface MultipleModulesRowProps { + moduleType: ModuleType + openEditModuleModal: (moduleType: ModuleType, moduleId?: string) => void + moduleOnDeckType?: ModuleType + moduleOnDeckModel?: ModuleModel + moduleOnDeck?: ModuleOnDeck[] +} + +export function MultipleModuleRow(props: MultipleModulesRowProps): JSX.Element { + const { + moduleOnDeck, + openEditModuleModal, + moduleOnDeckModel, + moduleOnDeckType, + moduleType, + } = props + const { t } = useTranslation(['modules', 'shared']) + const dispatch = useDispatch() + + const type: ModuleType = moduleOnDeckType ?? moduleType + const occupiedSlots = moduleOnDeck?.map(module => module.slot) ?? [] + const occupiedSlotsDisplayName = ( + moduleOnDeck?.map(module => module.slot) ?? [] + ).join(', ') + + const setCurrentModule = (moduleType: ModuleType, moduleId?: string) => () => + openEditModuleModal(moduleType, moduleId) + + const addRemoveText = moduleOnDeck ? t('shared:remove') : t('shared:add') + + const handleAddOrRemove = (): void => { + if (moduleOnDeck != null) { + moduleOnDeck.forEach(module => { + dispatch(stepFormActions.deleteModule(module.id)) + }) + } else { + setCurrentModule(type) + } + } + const handleEditModule = + moduleOnDeck && setCurrentModule(type, moduleOnDeck[0].id) + + return ( +
+

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

+
+
+ +
+
+ {moduleOnDeckModel && ( + + )} +
+
+ {occupiedSlots.length > 0 ? ( + + ) : null} +
+
+ {occupiedSlots.length > 0 ? ( + + ) : null} +
+
+ {moduleOnDeck != null ? ( + + {t('shared:edit')} + + ) : null} + + {addRemoveText} + +
+
+
+ ) +} diff --git a/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx b/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx new file mode 100644 index 000000000000..5d5d90794d54 --- /dev/null +++ b/protocol-designer/src/components/modules/__tests__/MultipleModuleRow.test.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' + +import { i18n } from '../../../localization' +import { renderWithProviders } from '../../../__testing-utils__' +import { MultipleModuleRow } from '../MultipleModuleRow' +import { + TEMPERATURE_MODULE_TYPE, + TEMPERATURE_MODULE_V2, +} from '@opentrons/shared-data' +import { FlexSlotMap } from '../FlexSlotMap' +import { deleteModule } from '../../../step-forms/actions' +import type { ModuleOnDeck } from '../../../step-forms' + +vi.mock('../../../step-forms/actions') +vi.mock('../FlexSlotMap') +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +const mockTemp: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, +} +const mockTemp2: ModuleOnDeck = { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'A1', + moduleState: {} as any, +} + +describe('MultipleModuleRow', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + moduleType: TEMPERATURE_MODULE_TYPE, + openEditModuleModal: vi.fn(), + moduleOnDeckType: TEMPERATURE_MODULE_TYPE, + moduleOnDeckModel: TEMPERATURE_MODULE_V2, + moduleOnDeck: [mockTemp, mockTemp2], + } + vi.mocked(FlexSlotMap).mockReturnValue(
mock FlexSlotMap
) + }) + it('renders 2 modules in the row with text and buttons', () => { + render(props) + screen.getByText('Multiple Temperatures') + screen.getByText('Position:') + screen.getByText('C3, A1') + screen.getByText('mock FlexSlotMap') + fireEvent.click(screen.getByText('edit')) + expect(props.openEditModuleModal).toHaveBeenCalled() + fireEvent.click(screen.getByText('remove')) + expect(vi.mocked(deleteModule)).toHaveBeenCalled() + }) + it('renders no modules', () => { + props.moduleOnDeck = undefined + render(props) + screen.getByText('add') + }) +}) diff --git a/protocol-designer/src/components/steplist/ModuleStepItems.tsx b/protocol-designer/src/components/steplist/ModuleStepItems.tsx index f3e91c1b73d3..548caf2964db 100644 --- a/protocol-designer/src/components/steplist/ModuleStepItems.tsx +++ b/protocol-designer/src/components/steplist/ModuleStepItems.tsx @@ -9,8 +9,9 @@ import { } from '@opentrons/components' import { PDListItem } from '../lists' import { LabwareTooltipContents } from './LabwareTooltipContents' +import type { ModuleType } from '@opentrons/shared-data' + import styles from './StepItem.module.css' -import { ModuleType } from '@opentrons/shared-data' export interface ModuleStepItemRowProps { label?: string | null @@ -31,44 +32,64 @@ export const ModuleStepItemRow = ( ) -interface Props { - action?: string +interface ModuleStepItemsProps { moduleType: ModuleType actionText: string - labwareNickname?: string | null - message?: string | null + moduleSlot?: string + action?: string children?: React.ReactNode hideHeader?: boolean + labwareNickname?: string | null + message?: string | null } -export const ModuleStepItems = (props: Props): JSX.Element => { - const { t } = useTranslation('modules') +export function ModuleStepItems(props: ModuleStepItemsProps): JSX.Element { + const { + moduleType, + actionText, + moduleSlot, + action, + hideHeader, + labwareNickname, + children, + message, + } = props + const { t } = useTranslation(['modules', 'application']) const [targetProps, tooltipProps] = useHoverTooltip({ placement: 'bottom-start', strategy: TOOLTIP_FIXED, }) + const moduleLongName = t(`module_long_names.${moduleType}`) + return ( <> - {!props.hideHeader && ( + {!Boolean(hideHeader) ? (
  • - {t(`module_long_names.${props.moduleType}`)} - {props.action} + + {moduleSlot != null + ? t('application:module_and_slot', { + moduleLongName, + slotName: moduleSlot, + }) + : moduleLongName} + + {action}
  • - )} + ) : null} - + - {props.children} - {props.message && ( + {children} + {message != null ? ( - "{props.message}" + "{message}" - )} + ) : null} ) } diff --git a/protocol-designer/src/components/steplist/StepItem.tsx b/protocol-designer/src/components/steplist/StepItem.tsx index c51502348a27..0fbb338cc0f1 100644 --- a/protocol-designer/src/components/steplist/StepItem.tsx +++ b/protocol-designer/src/components/steplist/StepItem.tsx @@ -25,6 +25,7 @@ import { makeTemperatureText, makeTimerText, } from '../../utils' +import { InitialDeckSetup } from '../../step-forms' import { PDListItem, TitledStepList } from '../lists' import { TitledListNotes } from '../TitledListNotes' import { AspirateDispenseHeader } from './AspirateDispenseHeader' @@ -121,11 +122,10 @@ export interface StepItemContentsProps { rawForm: FormData | null | undefined stepType: StepType substeps: SubstepItemData | null | undefined - ingredNames: WellIngredientNames labwareNicknamesById: { [labwareId: string]: string } additionalEquipmentEntities: AdditionalEquipmentEntities - + modules: InitialDeckSetup['modules'] highlightSubstep: (substepIdentifier: SubstepIdentifier) => unknown hoveredSubstep: SubstepIdentifier | null | undefined } @@ -293,6 +293,7 @@ export const StepItemContents = ( props: StepItemContentsProps ): JSX.Element | JSX.Element[] | null => { const { + modules, rawForm, stepType, substeps, @@ -326,6 +327,8 @@ export const StepItemContents = ( if (substeps && substeps.substepType === 'temperature') { const temperature = makeTemperatureText(substeps.temperature, t) + const moduleSlot = + substeps.moduleId != null ? modules[substeps.moduleId].slot : '' return ( ) } diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index bae70d17d7f7..b92192565c2e 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -65,10 +65,10 @@ export const DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP = 0 export const DEFAULT_DELAY_SECONDS = 1 export const DEFAULT_WELL_ORDER_FIRST_OPTION: 't2b' = 't2b' export const DEFAULT_WELL_ORDER_SECOND_OPTION: 'l2r' = 'l2r' -export const MIN_ENGAGE_HEIGHT_V1 = -5 -export const MAX_ENGAGE_HEIGHT_V1 = 40 -export const MIN_ENGAGE_HEIGHT_V2 = -4 -export const MAX_ENGAGE_HEIGHT_V2 = 19 +export const MIN_ENGAGE_HEIGHT_V1 = 0 +export const MAX_ENGAGE_HEIGHT_V1 = 45 +export const MIN_ENGAGE_HEIGHT_V2 = -2.5 +export const MAX_ENGAGE_HEIGHT_V2 = 25 export const MIN_TEMP_MODULE_TEMP = 4 export const MAX_TEMP_MODULE_TEMP = 95 export const MIN_HEATER_SHAKER_MODULE_TEMP = 37 diff --git a/protocol-designer/src/containers/ConnectedStepItem.tsx b/protocol-designer/src/containers/ConnectedStepItem.tsx index a6b4ceb1f261..a3ebcb05f417 100644 --- a/protocol-designer/src/containers/ConnectedStepItem.tsx +++ b/protocol-designer/src/containers/ConnectedStepItem.tsx @@ -24,7 +24,6 @@ import { SelectMultipleStepsAction, } from '../ui/steps' import { selectors as fileDataSelectors } from '../file-data' - import { StepItem, StepItemContents, @@ -38,12 +37,15 @@ import { ConfirmDeleteModal, DeleteModalType, } from '../components/modals/ConfirmDeleteModal' +import { + getAdditionalEquipmentEntities, + getInitialDeckSetup, +} from '../step-forms/selectors' -import { SubstepIdentifier } from '../steplist/types' -import { StepIdType } from '../form-types' -import { BaseState, ThunkAction } from '../types' -import { getAdditionalEquipmentEntities } from '../step-forms/selectors' -import { ThunkDispatch } from 'redux-thunk' +import type { ThunkDispatch } from 'redux-thunk' +import type { SubstepIdentifier } from '../steplist/types' +import type { StepIdType } from '../form-types' +import type { BaseState, ThunkAction } from '../types' export interface ConnectedStepItemProps { stepId: StepIdType @@ -86,7 +88,7 @@ export const ConnectedStepItem = ( const hasWarnings = hasTimelineWarningsPerStep[stepId] || hasFormLevelWarningsPerStep[stepId] - + const initialDeckSetup = useSelector(getInitialDeckSetup) const collapsed = useSelector(getCollapsedSteps)[stepId] const hoveredSubstep = useSelector(getHoveredSubstep) const hoveredStep = useSelector(getHoveredStepId) @@ -217,6 +219,7 @@ export const ConnectedStepItem = ( } const stepItemContentsProps: StepItemContentsProps = { + modules: initialDeckSetup.modules, rawForm: step, stepType: step.stepType, substeps, @@ -236,7 +239,6 @@ export const ConnectedStepItem = ( return CLOSE_STEP_FORM_WITH_CHANGES } } - return ( <> {showConfirmation && ( diff --git a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx index 4d03b5c16ac5..4156202796b5 100644 --- a/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx +++ b/protocol-designer/src/containers/__tests__/ConnectedStepItem.test.tsx @@ -1,5 +1,511 @@ -import { describe, it } from 'vitest' +import * as React from 'react' +import { describe, it, beforeEach, vi } from 'vitest' +import { screen } from '@testing-library/react' +import '@testing-library/jest-dom/vitest' +import { fixture96Plate, fixtureTiprack1000ul } from '@opentrons/shared-data' +import { renderWithProviders } from '../../__testing-utils__' +import { i18n } from '../../localization' +import { + getAdditionalEquipmentEntities, + getArgsAndErrorsByStepId, + getBatchEditFormHasUnsavedChanges, + getCurrentFormCanBeSaved, + getCurrentFormHasUnsavedChanges, + getInitialDeckSetup, + getOrderedStepIds, + getSavedStepForms, +} from '../../step-forms/selectors' +import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' +import { getErrorStepId, getSubsteps } from '../../file-data/selectors' +import { getHasTimelineWarningsPerStep } from '../../top-selectors/timelineWarnings' +import { getHasFormLevelWarningsPerStep } from '../../dismiss/selectors' +import { + getCollapsedSteps, + getHoveredSubstep, + getIsMultiSelectMode, + getMultiSelectItemIds, + getMultiSelectLastSelected, + getSelectedStepId, +} from '../../ui/steps' +import { getLabwareNicknamesById } from '../../ui/labware/selectors' +import { ConnectedStepItem } from '../ConnectedStepItem' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../../step-forms/selectors') +vi.mock('../../file-data/selectors') +vi.mock('../../top-selectors/timelineWarnings') +vi.mock('../../dismiss/selectors') +vi.mock('../../ui/steps') +vi.mock('../../labware-ingred/selectors') +vi.mock('../../ui/labware/selectors') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +const pauseStepId = 'pauseId' +const magnetStepId = 'magnetStepId' +const heaterShakerStepId = 'hsStepId' +const thermocyclerStepId = 'tcStepId' +const temperatureStepId = 'tempStepId' +const moveLabwareStepId = 'moveLabwareId' +const mixStepId = 'mixStepId' +const moveLiquidStepId = 'moveLiquidStepId' describe('ConnectedStepItem', () => { - it.todo('replace deprecated enzyme test') + let props: React.ComponentProps + beforeEach(() => { + props = { + stepId: pauseStepId, + stepNumber: 2, + onStepContextMenu: vi.fn(), + } + vi.mocked(getSavedStepForms).mockReturnValue({ + [pauseStepId]: { + stepType: 'pause', + id: pauseStepId, + pauseHour: '1', + pauseMinute: '10', + pauseSecond: '5', + pauseMessage: 'mock message', + pauseTemperature: '10', + }, + [magnetStepId]: { + stepType: 'magnet', + id: magnetStepId, + }, + [heaterShakerStepId]: { + stepType: 'heaterShaker', + id: heaterShakerStepId, + }, + [thermocyclerStepId]: { + stepType: 'thermocycler', + id: thermocyclerStepId, + }, + [temperatureStepId]: { + stepType: 'temperature', + id: temperatureStepId, + }, + [moveLabwareStepId]: { + stepType: 'moveLabware', + id: moveLabwareStepId, + }, + [mixStepId]: { + stepType: 'mix', + id: mixStepId, + }, + [moveLiquidStepId]: { + stepType: 'moveLiquid', + id: moveLiquidStepId, + }, + }) + vi.mocked(getArgsAndErrorsByStepId).mockReturnValue({ + [pauseStepId]: { + errors: false, + stepArgs: null, + }, + [magnetStepId]: { + errors: false, + stepArgs: null, + }, + [heaterShakerStepId]: { + errors: false, + stepArgs: null, + }, + [thermocyclerStepId]: { + errors: false, + stepArgs: null, + }, + [temperatureStepId]: { + errors: false, + stepArgs: null, + }, + [moveLabwareStepId]: { + errors: false, + stepArgs: null, + }, + [mixStepId]: { + errors: false, + stepArgs: null, + }, + [moveLiquidStepId]: { + errors: false, + stepArgs: null, + }, + }) + vi.mocked(getErrorStepId).mockReturnValue(null) + vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: false, + }) + vi.mocked(getHasFormLevelWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: false, + }) + vi.mocked(getInitialDeckSetup).mockReturnValue({ + pipettes: {}, + modules: { + thermocyclerId: { + id: 'thermocyclerId', + type: 'thermocyclerModuleType', + model: 'thermocyclerModuleV2', + slot: 'B1', + moduleState: {} as any, + }, + temperatureId: { + id: 'temperatureId', + type: 'temperatureModuleType', + model: 'temperatureModuleV2', + slot: 'C3', + moduleState: {} as any, + }, + heaterShakerId: { + id: 'heaterShakerId', + type: 'heaterShakerModuleType', + model: 'heaterShakerModuleV1', + slot: 'D1', + moduleState: {} as any, + }, + magnetId: { + id: 'magnetId', + type: 'magneticModuleType', + model: 'magneticModuleV2', + slot: 'C1', + moduleState: {} as any, + }, + }, + additionalEquipmentOnDeck: { + stagingAreaId: { + name: 'stagingArea', + location: 'B3', + id: 'stagingAreaId', + }, + }, + labware: { + labwareId: { + id: 'labwareId', + labwareDefURI: `opentrons/fixture_96_plate/1`, + slot: 'A2', + def: fixture96Plate as LabwareDefinition2, + }, + tipId: { + id: 'tipId', + labwareDefURI: `opentrons/${fixtureTiprack1000ul.parameters.loadName}/1`, + slot: 'D2', + def: fixtureTiprack1000ul as LabwareDefinition2, + }, + }, + }) + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getHoveredSubstep).mockReturnValue(null) + vi.mocked(getSelectedStepId).mockReturnValue(pauseStepId) + vi.mocked(getOrderedStepIds).mockReturnValue([ + pauseStepId, + magnetStepId, + heaterShakerStepId, + thermocyclerStepId, + moveLabwareStepId, + temperatureStepId, + mixStepId, + moveLiquidStepId, + ]) + vi.mocked(getMultiSelectItemIds).mockReturnValue(null) + vi.mocked(getMultiSelectLastSelected).mockReturnValue(null) + vi.mocked(getIsMultiSelectMode).mockReturnValue(false) + vi.mocked(getSubsteps).mockReturnValue({ + [pauseStepId]: { + substepType: 'pause', + pauseStepArgs: { + commandCreatorFnName: 'delay', + wait: 10, + name: 'pause', + description: '', + meta: { hours: 1, minutes: 10, seconds: 15 }, + }, + }, + [magnetStepId]: { + substepType: 'magnet', + engage: true, + labwareNickname: 'mockLabware', + message: 'engaging height', + }, + [heaterShakerStepId]: { + substepType: 'heaterShaker', + labwareNickname: 'mockLabware', + targetHeaterShakerTemperature: 20, + targetSpeed: 200, + latchOpen: false, + heaterShakerTimerMinutes: 5, + heaterShakerTimerSeconds: 11, + }, + [thermocyclerStepId]: { + substepType: 'thermocyclerProfile', + blockTargetTempHold: 30, + labwareNickname: 'mockLabware', + lidOpenHold: false, + lidTargetTempHold: 32, + meta: { rawProfileItems: [] }, + profileSteps: [ + { holdTime: 7, temperature: 87 }, + { holdTime: 2, temperature: 55 }, + ], + profileTargetLidTemp: 40, + profileVolume: 21, + }, + [temperatureStepId]: { + substepType: 'temperature', + temperature: 18, + labwareNickname: 'mockLabware', + moduleId: 'temperatureId', + message: 'mock message', + }, + [moveLabwareStepId]: { + substepType: 'moveLabware', + moveLabwareArgs: { + commandCreatorFnName: 'moveLabware', + name: 'move labware', + description: '', + labware: 'labwareId', + useGripper: false, + newLocation: { slotName: 'B2' }, + }, + }, + [mixStepId]: { + substepType: 'sourceDest', + multichannel: false, + commandCreatorFnName: 'mix', + parentStepId: mixStepId, + rows: [ + { + activeTips: null, + }, + ], + }, + [moveLiquidStepId]: { + substepType: 'sourceDest', + multichannel: false, + commandCreatorFnName: 'transfer', + parentStepId: moveLiquidStepId, + rows: [ + { + activeTips: { labwareId: 'tipId', wellName: 'A1' }, + substepIndex: 2, + source: { well: 'A1', preIngreds: {}, postIngreds: {} }, + dest: { well: 'A1', preIngreds: {}, postIngreds: {} }, + volume: 50, + }, + ], + }, + }) + vi.mocked(labwareIngredSelectors.getLiquidNamesById).mockReturnValue({}) + vi.mocked(getLabwareNicknamesById).mockReturnValue({}) + vi.mocked(getAdditionalEquipmentEntities).mockReturnValue({ + stagingAreaId: { name: 'stagingArea', location: 'B3', id: 'stagingArea' }, + }) + vi.mocked(getCurrentFormCanBeSaved).mockReturnValue(true) + vi.mocked(getCurrentFormHasUnsavedChanges).mockReturnValue(false) + vi.mocked(getBatchEditFormHasUnsavedChanges).mockReturnValue(false) + }) + it('renders an expanded step item for pause', () => { + render(props) + screen.getByText('2. pause') + screen.getByText('Pause for Time') + screen.getByText('1 h') + screen.getByText('10 m') + screen.getByText('15 s') + }) + it('renders an expanded step item for magnet', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(magnetStepId) + props.stepId = magnetStepId + render(props) + screen.getByText('2. magnet') + screen.getByText('Magnetic module') + screen.getByText('mockLabware') + screen.getByText('engage') + }) + it('renders an expanded step item for heater-shaker', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(heaterShakerStepId) + props.stepId = heaterShakerStepId + render(props) + screen.getByText('2. heater-shaker') + screen.getByText('Heater-Shaker module') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('20 °C') + screen.getByText('Labware Latch') + screen.getByText('Closed and Locked') + screen.getByText('Shaker') + screen.getByText('200 rpm') + screen.getByText('Deactivate after') + }) + it('renders an expanded step item for thermocycler', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(thermocyclerStepId) + props.stepId = thermocyclerStepId + render(props) + screen.getByText('2. thermocycler') + screen.getByText('Thermocycler module') + screen.getByText('profile') + screen.getByText('mockLabware') + screen.getByText('cycling') + screen.getByText('Lid (closed)') + screen.getByText('40 °C') + screen.getByText('Profile steps (0+ min)') + screen.getByText('Ending hold') + }) + it('renders an expanded step item for a temperature module', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: false, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(temperatureStepId) + props.stepId = temperatureStepId + render(props) + screen.getByText('2. temperature') + screen.getByText('Temperature module in Slot C3') + screen.getByText('go to') + screen.getByText('mockLabware') + screen.getByText('18 °C') + screen.getByText('"mock message"') + }) + it('renders an expanded step for move labware', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: false, + [mixStepId]: true, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLabwareStepId) + props.stepId = moveLabwareStepId + render(props) + screen.getByText('2. move labware') + screen.getByText('Manually') + screen.getByText('labware') + screen.getByText('new location') + }) + it('renders an expanded step for mix', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: false, + [moveLiquidStepId]: true, + }) + vi.mocked(getSelectedStepId).mockReturnValue(mixStepId) + props.stepId = mixStepId + render(props) + screen.getByText('2. mix') + screen.getByText('uL') + screen.getByText('μL') + }) + it('renders an expanded step for move liquid (transfer)', () => { + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLiquidStepId) + props.stepId = moveLiquidStepId + render(props) + screen.getByText('2. transfer') + screen.getByText('ASPIRATE') + screen.getByText('DISPENSE') + screen.getAllByText('A1') + screen.getByText('50 μL') + }) + it('renders a timeline warning icon for move liquid', () => { + vi.mocked(getHasTimelineWarningsPerStep).mockReturnValue({ + [pauseStepId]: false, + [magnetStepId]: false, + [heaterShakerStepId]: false, + [thermocyclerStepId]: false, + [temperatureStepId]: false, + [moveLabwareStepId]: false, + [mixStepId]: false, + [moveLiquidStepId]: true, + }) + vi.mocked(getCollapsedSteps).mockReturnValue({ + [pauseStepId]: true, + [magnetStepId]: true, + [heaterShakerStepId]: true, + [thermocyclerStepId]: true, + [temperatureStepId]: true, + [moveLabwareStepId]: true, + [mixStepId]: true, + [moveLiquidStepId]: false, + }) + vi.mocked(getSelectedStepId).mockReturnValue(moveLiquidStepId) + props.stepId = moveLiquidStepId + render(props) + screen.getByTestId('TitledStepList_icon_alert-circle') + }) }) diff --git a/protocol-designer/src/dismiss/actions.ts b/protocol-designer/src/dismiss/actions.ts index 772d69f02a4c..09f2c5a33c73 100644 --- a/protocol-designer/src/dismiss/actions.ts +++ b/protocol-designer/src/dismiss/actions.ts @@ -1,4 +1,5 @@ -import { StepIdType } from '../form-types' +import type { StepIdType } from '../form-types' + export interface DismissAction { type: ActionType payload: { @@ -6,6 +7,7 @@ export interface DismissAction { stepId: StepIdType } } + export type DismissFormWarning = DismissAction<'DISMISS_FORM_WARNING'> export type DismissTimelineWarning = DismissAction<'DISMISS_TIMELINE_WARNING'> export const dismissFormWarning = ( diff --git a/protocol-designer/src/form-types.ts b/protocol-designer/src/form-types.ts index 65d54b29ff92..f369844ad375 100644 --- a/protocol-designer/src/form-types.ts +++ b/protocol-designer/src/form-types.ts @@ -78,6 +78,12 @@ export type StepFieldName = string // | 'dispense_touchTip' // | 'aspirate_disposalVol_checkbox' // | 'aspirate_disposalVol_volume' +// | 'aspirate_x_position +// | 'aspirate_y_position +// | 'dispense_x_position +// | 'dispense_y_position +// | 'mix_x_position +// | 'mix_y_position // TODO Ian 2019-01-16 factor out to some constants.js ? See #2926 export type StepType = | 'moveLabware' @@ -222,6 +228,11 @@ export interface HydratedMoveLiquidFormData { blowout_location: string | null | undefined // labwareId or 'SOURCE_WELL' or 'DEST_WELL' dropTip_location: string nozzles: NozzleConfigurationStyle | null + aspirate_x_position?: number | null + aspirate_y_position?: number | null + dispense_x_position?: number | null + dispense_y_position?: number | null + blowout_z_offset?: number | null } } @@ -263,6 +274,9 @@ export interface HydratedMixFormDataLegacy { dispense_delay_seconds: number | null | undefined dropTip_location: string nozzles: NozzleConfigurationStyle | null + mix_x_position?: number | null + mix_y_position?: number | null + blowout_z_offset?: number | null } export type MagnetAction = 'engage' | 'disengage' export type HydratedMagnetFormData = AnnotationFields & { @@ -303,7 +317,7 @@ export type HydratedMoveLiquidFormDataLegacy = AnnotationFields & stepType: 'moveLiquid' } // fields used in TipPositionInput -export type TipOffsetFields = +export type TipZOffsetFields = | 'aspirate_mmFromBottom' | 'dispense_mmFromBottom' | 'mix_mmFromBottom' @@ -312,6 +326,17 @@ export type TipOffsetFields = | 'aspirate_delay_mmFromBottom' | 'dispense_delay_mmFromBottom' | 'mix_touchTip_mmFromBottom' + +export type TipYOffsetFields = + | 'aspirate_y_position' + | 'dispense_y_position' + | 'mix_y_position' + +export type TipXOffsetFields = + | 'aspirate_x_position' + | 'dispense_x_position' + | 'mix_x_position' + export type DelayCheckboxFields = | 'aspirate_delay_checkbox' | 'dispense_delay_checkbox' diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen1.gif b/protocol-designer/src/images/modules/engage_height_animation_gen1.gif deleted file mode 100644 index eb0bedae5735..000000000000 Binary files a/protocol-designer/src/images/modules/engage_height_animation_gen1.gif and /dev/null differ diff --git a/protocol-designer/src/images/modules/engage_height_animation_gen2.gif b/protocol-designer/src/images/modules/engage_height_animation_gen2.gif deleted file mode 100644 index 2865ccb11181..000000000000 Binary files a/protocol-designer/src/images/modules/engage_height_animation_gen2.gif and /dev/null differ diff --git a/protocol-designer/src/images/modules/engage_height_static_gen1.png b/protocol-designer/src/images/modules/engage_height_static_gen1.png deleted file mode 100644 index 1bb216a5f06e..000000000000 Binary files a/protocol-designer/src/images/modules/engage_height_static_gen1.png and /dev/null differ diff --git a/protocol-designer/src/images/modules/engage_height_static_gen2.png b/protocol-designer/src/images/modules/engage_height_static_gen2.png deleted file mode 100644 index 9e9e163371b7..000000000000 Binary files a/protocol-designer/src/images/modules/engage_height_static_gen2.png and /dev/null differ diff --git a/protocol-designer/src/load-file/migration/8_1_0.ts b/protocol-designer/src/load-file/migration/8_1_0.ts index c60ffb211832..755ef2a3ed2c 100644 --- a/protocol-designer/src/load-file/migration/8_1_0.ts +++ b/protocol-designer/src/load-file/migration/8_1_0.ts @@ -54,7 +54,7 @@ export const migrateFile = ( form => form.stepType === 'moveLiquid' || form.stepType === 'mix' ) - const pipettingSavedStepsWithTipRack = pipettingSavedSteps.reduce( + const pipettingSavedStepsWithAdditionalFields = pipettingSavedSteps.reduce( (acc, item) => { const tipRackUri = tiprackAssignments[item.pipette] const tiprackLoadName = @@ -67,8 +67,21 @@ export const migrateFile = ( const tiprackIds = loadLabwareCommands .filter(command => command.params.loadName === tiprackLoadName) .map(command => command.params.labwareId) - - acc[item.id] = { ...item, tipRack: tiprackIds[0] } + const xyKeys = + item.stepType === 'mix' + ? { mix_x_position: 0, mix_y_position: 0 } + : { + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, + } + acc[item.id] = { + ...item, + blowout_z_offset: 0, + tipRack: tiprackIds[0], + ...xyKeys, + } return acc }, {} @@ -82,7 +95,7 @@ export const migrateFile = ( ...designerApplication.data, savedStepForms: { ...designerApplication.data.savedStepForms, - ...pipettingSavedStepsWithTipRack, + ...pipettingSavedStepsWithAdditionalFields, }, pipetteTiprackAssignments: newTiprackAssignments, }, diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 272e51a93636..34ac8c33a026 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -49,10 +49,14 @@ "title": "Missing labware", "body": "Your module has no labware on it. We recommend you add labware before proceeding." }, - "export_v8_protocol_7_1": { + "multiple_modules_without_labware": { + "title": "Missing labware", + "body": "One or more module has no labware on it. We recommend you add labware before proceeding" + }, + "export_v8_1_protocol_7_3": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", - "body2": "7.1 or higher", + "body2": "7.3.0 or higher", "body3": ". Please ensure your robot is updated to the correct version." }, "change_magnet_module_model": { @@ -214,6 +218,11 @@ "export": "Export", "import": "Import", "module_placement": { + "SLOTS_OCCUPIED": { + "title": "Cannot place module", + "single": "Slot {{slotName}} is occupied", + "multi": "Multiple slots are occupied" + }, "SLOT_OCCUPIED": { "title": "Cannot place module", "body": "Slot {{selectedSlot}} is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue." @@ -256,12 +265,12 @@ }, "unused_module": { "heading": "Unused module", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", + "body1": "The {{modulesDetails}} specified in your protocol in Slot {{slotName}} is not currently used in any step. In order to run this protocol you will need to power up and connect the module to your robot.", "body2": "If you don't intend to use the module, please consider removing it from your protocol." }, "unused_modules": { "heading": "Unused modules", - "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", + "body1": "One or more modules specified in your protocol in Slot(s) {{slotName}} are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", "body2": "If you don't intend to use these modules, please consider removing them from your protocol." }, "unused_gripper": { diff --git a/protocol-designer/src/localization/en/application.json b/protocol-designer/src/localization/en/application.json index dfa905ea70c3..7943a006e6fd 100644 --- a/protocol-designer/src/localization/en/application.json +++ b/protocol-designer/src/localization/en/application.json @@ -15,6 +15,8 @@ "update": "UPDATE", "updated": "UPDATED", "pipettes": "Pipettes", + "magnet_height_caption": "Must be between {{low}} to {{high}}.", + "magnet_recommended": "The recommended height is {{default}}", "networking": { "generic_verification_failure": "Something went wrong with your unique link. Fill out the form below to have a new one sent to your email. Please contact the Opentrons Support team if you require further help.", "unauthorized_verification_failure": "This unique link has expired and is no longer valid, to have a new link sent to your email, fill out the form below.", @@ -23,6 +25,7 @@ "next": "Next", "no_batch_edit_shared_settings": "Batch editing of settings is only available for Transfer or Mix steps", "manually": "Manually", + "module_and_slot": "{{moduleLongName}} in Slot {{slotName}}", "stepType": { "mix": "mix", "moveLabware": "move labware", diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index edceb80718f6..6d51439a8287 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -42,6 +42,14 @@ "deckConfigAnd96Channel": { "body1": "Introducing the {{pd}} 8.0 with deck configuration and 96-channel pipette support!", "body2": "All protocols now require {{app}} version 7.1+ to run." + }, + "customParamsAndMultiTipAndModule": { + "body1": "Introducing {{pd}} 8.1. Starting today, you will be able to:", + "body2": "Customize blowout speed and height.", + "body3": "Adjust horizontal position within a well when aspirating, dispensing, or mixing.", + "body4": "Assign up to three types of tip racks to a single pipette.", + "body5": "Add multiple Temperature Modules to the deck (Flex only).", + "body6": "All protocols require {{app}} version 7.3.0 or later to run." } }, "labware_selection": { @@ -61,7 +69,16 @@ }, "tip_position": { "title": "Tip Positioning", + "caption": "between {{min}} and {{max}}", + "warning": "The X and/or Y position value is close to edge of the well and might collide with it", + "radio_button": { + "default": "{{defaultMmFromBottom}} mm from the bottom center (default)", + "blowout": "0 mm from the top center (default)", + "mix": "Aspirate 1mm, Dispense 0.5mm from the bottom center (default)", + "custom": "Custom" + }, "body": { + "blowout_z_offset": "Change from where in the well the robot emits blowout", "aspirate_mmFromBottom": "Change from where in the well the robot aspirates", "dispense_mmFromBottom": "Change from where in the well the robot dispenses", "mix_mmFromBottom": "Change from where in the well the robot aspirates and dispenses during the mix", @@ -71,9 +88,14 @@ "aspirate_delay_mmFromBottom": "Change from where in the well the robot delays after aspirating", "dispense_delay_mmFromBottom": "Change from where in the well the robot delays after dispensing" }, + "field_titles": { + "z_position": "Z position", + "x_position": "X position", + "y_position": "Y position" + }, "errors": { "TOO_MANY_DECIMALS": "a max of 1 decimal place is allowed", - "OUT_OF_BOUNDS": "accepted range is {{minMmFromBottom}} to {{maxMmFromBottom}}" + "OUT_OF_BOUNDS": "accepted range is {{minMm}} to {{maxMm}}" }, "field_label": "Distance from bottom of well" }, @@ -155,7 +177,7 @@ "closeUnsavedStepForm": { "title": "Unsaved step form", "body": "You have not saved this step form. If you navigate away without saving, this step will be deleted.", - "confirm_button": "delete step" + "confirm_button": "discard step" }, "closeBatchEditForm": { "title": "Unsaved changes to multiple steps", diff --git a/protocol-designer/src/localization/en/modules.json b/protocol-designer/src/localization/en/modules.json index 10a50dc07752..5cad25ca0508 100644 --- a/protocol-designer/src/localization/en/modules.json +++ b/protocol-designer/src/localization/en/modules.json @@ -6,6 +6,7 @@ "wasteChute": "Waste Chute" }, "module_display_names": { + "multipleTemperatureModuleTypes": "Multiple Temperatures", "temperatureModuleType": "Temperature", "magneticModuleType": "Magnetic", "thermocyclerModuleType": "Thermocycler", diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index d69d55ffe324..89d916bce357 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -1,5 +1,6 @@ { "add": "add", + "amount": "Amount:", "confirm_reorder": "Are you sure you want to reorder these steps, it may cause errors?", "edit": "edit", "exit": "exit", diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 59d2f32d1c9a..8e293d8efdde 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -4,9 +4,11 @@ "disabled_cannot_delete_trash": "A Trash Bin or Waste Chute is required", "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", - "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", + "disabled_no_space_additional_items": "No space for this combination of staging area slots, trash, and modules.", "disabled_you_can_add_one_type": "Only one module of each type is allowed on the deck at a time", + "not_enough_space_for_temp": "There is not enough space on the deck to add more temperature modules", "not_in_beta": "ⓘ Coming Soon", + "missing_tiprack": "Missing a tiprack? Make sure it is added to the deck", "step_description": { "heaterShaker": "Set heat, shake, or labware latch commands for the Heater-Shaker module", @@ -26,7 +28,7 @@ "aspirate_delay_mmFromBottom": "Distance from the bottom of the well", "aspirate_flowRate": "The speed at which the pipette aspirates", "aspirate_mix_checkbox": "Pipette up and down before aspirating", - "aspirate_mmFromBottom": "Distance from the bottom of the well", + "aspirate_mmFromBottom": "Adjust tip position for aspirate", "aspirate_touchTip_checkbox": "Touch tip to each side of well after aspirating", "aspirate_touchTip_mmFromBottom": "Distance from the bottom of the well", "blowout_checkbox": "Where to dispose of remaining volume in tip", @@ -37,16 +39,17 @@ "dispense_delay_mmFromBottom": "Distance from the bottom of the well", "dispense_flowRate": "The speed at which the pipette dispenses", "dispense_mix_checkbox": "Pipette up and down after dispensing", - "dispense_mmFromBottom": "Distance from the bottom of the well", + "dispense_mmFromBottom": "Adjust tip position for dispense", "dispense_touchTip_checkbox": "Touch tip to each side of well after dispensing", "dispense_touchTip_mmFromBottom": "Distance from the bottom of the well", "disposalVolume_checkbox": "Aspirate extra volume that is disposed of after a multi-dispense is complete. We recommend a disposal volume of at least the pipette's minimum.", "heaterShakerSetTimer": "Once this counter has elapsed, the module will deactivate the heater and shaker", - "mix_mmFromBottom": "Distance from the bottom of the well", + "mix_mmFromBottom": "Adjust tip position", "mix_touchTip_checkbox": "Touch tip to each side of the well after mixing", "mix_touchTip_mmFromBottom": "Distance from the bottom of the well", "preWetTip": "Pre-wet pipette tip by aspirating and dispensing 2/3 of the tip's max volume", - "volume": "Volume to dispense in each well" + "volume": "Volume to dispense in each well", + "blowout_z_offset": "The height at which blowout occurs from the top of the well" }, "indeterminate": { "aspirate_airGap_checkbox": "Not all selected steps are using this setting", @@ -61,14 +64,22 @@ "mix_touchTip_checkbox": "Not all selected steps are using this setting", "preWetTip": "Not all selected steps are using this setting" }, + "mix": { + "disabled": { + "mix_mmFromBottom": "Tip position adjustment is not supported", + "blowout_z_offset": "Blowout location and destination labware must first be selected" + } + }, "moveLiquid": { "disabled": { "$generic": "Incompatible with current path", "aspirate_touchTip_checkbox": "Touch tip is not supported", "blowout_checkbox": "Redundant with disposal volume", "dispense_mix_checkbox": "Unable to mix in a waste chute or trash bin", + "aspirate_mmFromBottom": "Tip position adjustment is not supported", "dispense_mmFromBottom": "Tip position adjustment is not supported", - "dispense_touchTip_checkbox": "Touch tip is not supported" + "dispense_touchTip_checkbox": "Touch tip is not supported", + "blowout_z_offset": "Blowout location, source, and destination labware must first be selected" } }, "moveLabware": { diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index a19cd27eeac2..5d42a31b0860 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -67,8 +67,10 @@ import { createPresavedStepForm, getDeckItemIdInSlot, getIdsInRange, + getUnoccupiedSlotForMoveableTrash, } from '../utils' -import { + +import type { CreateModuleAction, CreatePipettesAction, DeleteModuleAction, @@ -1379,6 +1381,12 @@ export const additionalEquipmentInvariantProperties = handleActions { const stagingAreaId = `${uuid()}:stagingArea` const cutoutId = getCutoutIdByAddressableArea( @@ -1531,11 +1539,11 @@ export const additionalEquipmentInvariantProperties = handleActions getLabwareDisplayName(def) ), @@ -455,7 +454,10 @@ export const getModulesForEditModulesCard: Selector< reduce( initialDeckSetup.modules, (acc, moduleOnDeck: ModuleOnDeck, id) => { - acc[moduleOnDeck.type] = moduleOnDeck + if (!acc[moduleOnDeck.type]) { + acc[moduleOnDeck.type] = [] + } + acc[moduleOnDeck.type]?.push(moduleOnDeck) return acc }, { diff --git a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts index 526c4c784b1e..11b93d8dbc65 100644 --- a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts +++ b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts @@ -187,6 +187,11 @@ describe('createPresavedStepForm', () => { stepDetails: '', stepName: 'transfer', volume: null, + aspirate_x_position: 0, + aspirate_y_position: 0, + dispense_x_position: 0, + dispense_y_position: 0, + blowout_z_offset: 0, }) }) describe('mix step', () => { @@ -210,6 +215,9 @@ describe('createPresavedStepForm', () => { mix_wellOrder_first: 't2b', mix_wellOrder_second: 'l2r', blowout_checkbox: false, + mix_x_position: 0, + mix_y_position: 0, + blowout_z_offset: 0, blowout_location: null, changeTip: 'always', stepDetails: '', diff --git a/protocol-designer/src/step-forms/test/utils.test.ts b/protocol-designer/src/step-forms/test/utils.test.ts index f848fe1241d2..d7dbe1960b57 100644 --- a/protocol-designer/src/step-forms/test/utils.test.ts +++ b/protocol-designer/src/step-forms/test/utils.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect } from 'vitest' -import { getIdsInRange } from '../utils' +import { getIdsInRange, getUnoccupiedSlotForMoveableTrash } from '../utils' +import type { AddressableAreaName, CreateCommand } from '@opentrons/shared-data' + describe('getIdsInRange', () => { it('gets id in array of length 1', () => { expect(getIdsInRange(['X'], 'X', 'X')).toEqual(['X']) @@ -29,3 +31,126 @@ describe('getIdsInRange', () => { expect(getIdsInRange(orderedIds, 'T', 'T')).toEqual(['T']) }) }) +describe('getUnoccupiedSlotForMoveableTrash', () => { + it('returns slot C1 when all other slots are occupied by modules, labware, moveLabware, and staging areas', () => { + const mockPDFile: any = { + commands: [ + { + key: '7353ae60-c85e-45c4-8d69-59ff3a97debd', + commandType: 'loadModule', + params: { + model: 'thermocyclerModuleV2', + location: { slotName: 'B1' }, + moduleId: + '771f390f-01a9-4615-9c4e-4dbfc95844b5:thermocyclerModuleType', + }, + }, + { + key: '82e5d08f-ceae-4eb8-8600-b61a973d47d9', + commandType: 'loadModule', + params: { + model: 'heaterShakerModuleV1', + location: { slotName: 'D1' }, + moduleId: + 'b9df03af-3844-4ae8-a1cf-cae61a6b4992:heaterShakerModuleType', + }, + }, + { + key: '49bc2a29-a7d2-42a6-8610-e07a9ad166df', + commandType: 'loadModule', + params: { + model: 'temperatureModuleV2', + location: { slotName: 'D3' }, + moduleId: + '52bea856-eea6-473c-80df-b316f3559692:temperatureModuleType', + }, + }, + { + key: '864fadd7-f2c1-400a-b2ef-24d0c887a3c8', + commandType: 'loadLabware', + params: { + displayName: 'Opentrons Flex 96 Tip Rack 50 µL', + labwareId: + '88881828-037c-4445-ba57-121164f4a53a:opentrons/opentrons_flex_96_tiprack_50ul/1', + loadName: 'opentrons_flex_96_tiprack_50ul', + namespace: 'opentrons', + version: 1, + location: { slotName: 'C2' }, + }, + }, + { + key: '79994418-d664-4884-9441-4b0fa62bd143', + commandType: 'loadLabware', + params: { + displayName: 'Bio-Rad 96 Well Plate 200 µL PCR', + labwareId: + '733c04a8-ae8c-449f-a1f9-ca3783fdda58:opentrons/biorad_96_wellplate_200ul_pcr/2', + loadName: 'biorad_96_wellplate_200ul_pcr', + namespace: 'opentrons', + version: 2, + location: { addressableAreaName: 'A4' }, + }, + }, + { + key: 'b2170a2c-d202-4129-9cd7-ffa4e35d57bb', + commandType: 'loadLabware', + params: { + displayName: 'Corning 24 Well Plate 3.4 mL Flat', + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + loadName: 'corning_24_wellplate_3.4ml_flat', + namespace: 'opentrons', + version: 2, + location: { slotName: 'B3' }, + }, + }, + { + key: 'fb1807fe-ca16-4f75-b44d-803d704c7d98', + commandType: 'loadLabware', + params: { + displayName: 'Opentrons Flex 96 Tip Rack 50 µL', + labwareId: + '11fdsa8b1-bf4b-4a6c-80cb-b8e5bdfe309b:opentrons/opentrons_flex_96_tiprack_50ul/1', + loadName: 'opentrons_flex_96_tiprack_50ul', + namespace: 'opentrons', + version: 1, + location: { + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + }, + }, + }, + { + commandType: 'moveLabware', + key: '1395243a-958f-4305-9687-52cdaf39f2b6', + params: { + labwareId: + '733c04a8-ae8c-449f-a1f9-ca3783fdda58:opentrons/biorad_96_wellplate_200ul_pcr/2', + strategy: 'usingGripper', + newLocation: { slotName: 'C1' }, + }, + }, + { + commandType: 'moveLabware', + key: '4e39e7ec-4ada-4e3c-8369-1ff7421061a9', + params: { + labwareId: + '32e97c67-866e-4153-bcb7-2b86b1d3f1fe:opentrons/corning_24_wellplate_3.4ml_flat/2', + strategy: 'usingGripper', + newLocation: { addressableAreaName: 'A4' }, + }, + }, + ] as CreateCommand[], + } + const mockStagingAreaSlotNames: AddressableAreaName[] = ['A4', 'B4'] + const mockHasWasteChuteCommands = false + + expect( + getUnoccupiedSlotForMoveableTrash( + mockPDFile, + mockHasWasteChuteCommands, + mockStagingAreaSlotNames + ) + ).toStrictEqual('C3') + }) +}) diff --git a/protocol-designer/src/step-forms/types.ts b/protocol-designer/src/step-forms/types.ts index 81422cc985b2..24dee9b0c46f 100644 --- a/protocol-designer/src/step-forms/types.ts +++ b/protocol-designer/src/step-forms/types.ts @@ -72,7 +72,7 @@ export interface ModuleTemporalProperties { } export type ModuleOnDeck = ModuleEntity & ModuleTemporalProperties export type ModulesForEditModulesCard = Partial< - Record + Record > // =========== LABWARE ======== export type NormalizedLabwareById = Record< diff --git a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts index 68e2f1511726..6b5fc39fbadb 100644 --- a/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts +++ b/protocol-designer/src/step-forms/utils/getProfileItemsHaveErrors.ts @@ -1,5 +1,6 @@ import { getProfileFieldErrors } from '../../steplist/fieldLevel' -import { ProfileItem, PROFILE_CYCLE } from '../../form-types' +import { PROFILE_CYCLE } from '../../form-types' +import type { ProfileItem } from '../../form-types' const _someFieldsHaveErrors = (item: ProfileItem): boolean => { for (const fieldName in item) { diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index 73596b481c65..d9b2d108132e 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -6,20 +6,23 @@ import { getPipetteSpecsV2, GEN_ONE_MULTI_PIPETTES, THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V2, + WASTE_CHUTE_CUTOUT, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { SPAN7_8_10_11_SLOT, TC_SPAN_SLOTS } from '../../constants' import { hydrateField } from '../../steplist/fieldLevel' import { LabwareDefByDefURI } from '../../labware-defs' -import type { DeckSlotId, ModuleType } from '@opentrons/shared-data' +import { getCutoutIdByAddressableArea } from '../../utils' import type { - AdditionalEquipmentOnDeck, - InitialDeckSetup, - ModuleOnDeck, - FormPipettesByMount, - FormPipette, - LabwareOnDeck as LabwareOnDeckType, -} from '../types' -import type { DeckSlot } from '../../types' + AddressableAreaName, + CutoutId, + DeckSlotId, + LoadLabwareCreateCommand, + LoadModuleCreateCommand, + ModuleType, + MoveLabwareCreateCommand, +} from '@opentrons/shared-data' import type { NormalizedPipette, NormalizedPipetteById, @@ -28,9 +31,54 @@ import type { InvariantContext, ModuleEntity, } from '@opentrons/step-generation' +import type { DeckSlot } from '../../types' import type { FormData } from '../../form-types' +import type { PDProtocolFile } from '../../file-types' +import type { + AdditionalEquipmentOnDeck, + InitialDeckSetup, + ModuleOnDeck, + FormPipettesByMount, + FormPipette, + LabwareOnDeck as LabwareOnDeckType, +} from '../types' export { createPresavedStepForm } from './createPresavedStepForm' +const MOVABLE_TRASH_CUTOUTS = [ + { + value: 'cutoutA3', + slot: 'A3', + }, + { + value: 'cutoutA1', + slot: 'A1', + }, + { + value: 'cutoutB1', + slot: 'B1', + }, + { + value: 'cutoutB3', + slot: 'B3', + }, + { + value: 'cutoutC1', + slot: 'C1', + }, + { + value: 'cutoutC3', + slot: 'C3', + }, + { + value: 'cutoutD1', + slot: 'D1', + }, + { + value: 'cutoutD3', + slot: 'D3', + }, +] + const slotToCutoutOt2Map: { [key: string]: string } = { '1': 'cutout1', '2': 'cutout2', @@ -248,3 +296,82 @@ export function getHydratedForm( // @ts-expect-error(sa, 2021-6-14):type this properly in #3161 return hydratedForm } + +export const getUnoccupiedSlotForMoveableTrash = ( + file: PDProtocolFile, + hasWasteChuteCommands: boolean, + stagingAreaSlotNames: AddressableAreaName[] +): string => { + const wasteChuteSlot = hasWasteChuteCommands ? [WASTE_CHUTE_CUTOUT] : [] + const stagingAreaCutoutIds = stagingAreaSlotNames.map(slotName => + getCutoutIdByAddressableArea( + slotName, + 'stagingAreaRightSlot', + FLEX_ROBOT_TYPE + ) + ) + const allLoadLabwareSlotNames = Object.values(file.commands) + .filter( + (command): command is LoadLabwareCreateCommand => + command.commandType === 'loadLabware' + ) + .reduce((acc: string[], command) => { + const location = command.params.location + if ( + location !== 'offDeck' && + location !== null && + 'slotName' in location + ) { + return [...acc, location.slotName] + } + return acc + }, []) + + const allLoadModuleSlotNames = Object.values(file.commands) + .filter( + (command): command is LoadModuleCreateCommand => + command.commandType === 'loadModule' + ) + .flatMap(command => { + // special-casing Thermocycler + if (command.params.model === THERMOCYCLER_MODULE_V2) { + return ['A1', command.params.location.slotName] + } else { + return command.params.location.slotName + } + }) + + const allMoveLabwareLocations = Object.values(file.commands) + .filter( + (command): command is MoveLabwareCreateCommand => + command.commandType === 'moveLabware' + ) + .reduce((acc: string[], command) => { + const newLocation = command.params.newLocation + if ( + newLocation !== 'offDeck' && + newLocation !== null && + 'slotName' in newLocation + ) { + return [...acc, newLocation.slotName] + } + return acc + }, []) + + const unoccupiedSlot = MOVABLE_TRASH_CUTOUTS.find( + cutout => + !allLoadLabwareSlotNames.includes(cutout.slot) && + !allLoadModuleSlotNames.includes(cutout.slot) && + !allMoveLabwareLocations.includes(cutout.slot) && + !wasteChuteSlot.includes(cutout.value as typeof WASTE_CHUTE_CUTOUT) && + !stagingAreaCutoutIds.includes(cutout.value as CutoutId) + ) + if (unoccupiedSlot == null) { + console.error( + 'Expected to find an unoccupied slot for auto-generating a trash bin but could not' + ) + return '' + } + + return unoccupiedSlot.slot +} diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 66656441dd14..b90eb6f028ee 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -4,6 +4,7 @@ import { DEFAULT_WELL_ORDER_FIRST_OPTION, DEFAULT_WELL_ORDER_SECOND_OPTION, DEFAULT_DELAY_SECONDS, + DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } from '../../constants' import { StepType, StepFieldName } from '../../form-types' export function getDefaultsForStepType( @@ -37,6 +38,9 @@ export function getDefaultsForStepType( dropTip_location: null, nozzles: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLiquid': @@ -86,6 +90,11 @@ export function getDefaultsForStepType( dispense_delay_mmFromBottom: null, dropTip_location: null, nozzles: null, + dispense_x_position: 0, + dispense_y_position: 0, + aspirate_x_position: 0, + aspirate_y_position: 0, + blowout_z_offset: DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts index 16765d26436e..d480b455666f 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMixForm.ts @@ -1,3 +1,4 @@ +import { DEST_WELL_BLOWOUT_DESTINATION } from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -21,5 +22,14 @@ export function getDisabledFieldsMixForm( disabled.add('mix_touchTip_checkbox') } + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts index ec514c81cced..5ca7db1395f7 100644 --- a/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts +++ b/protocol-designer/src/steplist/formLevel/getDisabledFields/getDisabledFieldsMoveLiquidForm.ts @@ -1,3 +1,7 @@ +import { + DEST_WELL_BLOWOUT_DESTINATION, + SOURCE_WELL_BLOWOUT_DESTINATION, +} from '@opentrons/step-generation' import type { HydratedFormdata } from '../../../form-types' // NOTE: expects that '_checkbox' fields are implemented so that // when checkbox is disabled, its dependent fields are hidden @@ -37,5 +41,17 @@ export function getDisabledFieldsMoveLiquidForm( disabled.add(prefix + '_wells') } }) + + if ( + !hydratedForm.blowout_location || + hydratedForm.blowout_location.includes('wasteChute') || + hydratedForm.blowout_location.includes('trashBin') || + (hydratedForm.blowout_location === SOURCE_WELL_BLOWOUT_DESTINATION && + !hydratedForm.aspirate_labware) || + (hydratedForm.blowout_location === DEST_WELL_BLOWOUT_DESTINATION && + !hydratedForm.dispense_labware) + ) { + disabled.add('blowout_z_offset') + } return disabled } diff --git a/protocol-designer/src/steplist/formLevel/index.ts b/protocol-designer/src/steplist/formLevel/index.ts index 669e048ab4ef..64c4fbff39b4 100644 --- a/protocol-designer/src/steplist/formLevel/index.ts +++ b/protocol-designer/src/steplist/formLevel/index.ts @@ -29,6 +29,9 @@ import { minDisposalVolume, minAspirateAirGapVolume, minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from './warnings' import { HydratedFormdata, StepType } from '../../form-types' @@ -52,7 +55,10 @@ interface FormHelpers { const stepFormHelperMap: Partial> = { mix: { getErrors: composeErrors(incompatibleLabware, volumeTooHigh), - getWarnings: composeWarnings(belowPipetteMinimumVolume), + getWarnings: composeWarnings( + belowPipetteMinimumVolume, + mixTipPositionInTube + ), }, pause: { getErrors: composeErrors(pauseForTimeOrUntilTold), @@ -68,7 +74,9 @@ const stepFormHelperMap: Partial> = { maxDispenseWellVolume, minDisposalVolume, minAspirateAirGapVolume, - minDispenseAirGapVolume + minDispenseAirGapVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube ), }, magnet: { diff --git a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts index 28828c7524d7..b9ee871772d1 100644 --- a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts +++ b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts @@ -1,8 +1,9 @@ -import { LabwareLocation } from '@opentrons/shared-data' +import { getLabwareDefIsStandard } from '@opentrons/shared-data' import { COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE, COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER, } from '../../utils/labwareModuleCompatibility' +import type { LabwareLocation } from '@opentrons/shared-data' import type { InvariantContext, LabwareEntity, @@ -11,15 +12,18 @@ import type { ProfileFormError } from './profileErrors' type HydratedFormData = any -// TODO(Jr, 1/16/24): look into the use case of this util since the i18n strings -// previously listed in this util were not found in any json. const getMoveLabwareError = ( labware: LabwareEntity, newLocation: LabwareLocation, invariantContext: InvariantContext ): string | null => { let errorString: string | null = null - if (labware == null || newLocation == null || newLocation === 'offDeck') + if ( + labware == null || + newLocation == null || + newLocation === 'offDeck' || + !getLabwareDefIsStandard(labware?.def) + ) return null const selectedLabwareDefUri = labware?.labwareDefURI if ('moduleId' in newLocation) { diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 741355f95a01..d28f6dc42dfa 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -15,7 +15,15 @@ type MixStepArgs = MixArgs export const mixFormToArgs = ( hydratedFormData: HydratedMixFormDataLegacy ): MixStepArgs => { - const { labware, pipette, dropTip_location, nozzles } = hydratedFormData + const { + labware, + pipette, + dropTip_location, + nozzles, + mix_x_position, + mix_y_position, + blowout_z_offset, + } = hydratedFormData const matchingTipLiquidSpecs = getMatchingTipLiquidSpecs( pipette, hydratedFormData.volume, @@ -66,7 +74,7 @@ export const mixFormToArgs = ( matchingTipLiquidSpecs?.defaultBlowOutFlowRate.default const blowoutOffsetFromTopMm = blowoutLocation - ? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP : 0 // Delay settings const aspirateDelaySeconds = getMixDelayData( @@ -105,5 +113,9 @@ export const mixFormToArgs = ( dispenseDelaySeconds, dropTipLocation: dropTip_location, nozzles, + aspirateXOffset: mix_x_position ?? 0, + dispenseXOffset: mix_x_position ?? 0, + aspirateYOffset: mix_y_position ?? 0, + dispenseYOffset: mix_y_position ?? 0, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 7d330f54dbfd..05910f133323 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -78,6 +78,11 @@ export const moveLiquidFormToArgs = ( path, tipRack, nozzles, + aspirate_x_position, + dispense_x_position, + aspirate_y_position, + dispense_y_position, + blowout_z_offset, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -161,7 +166,10 @@ export const moveLiquidFormToArgs = ( ) const blowoutLocation = (fields.blowout_checkbox && fields.blowout_location) || null - const blowoutOffsetFromTopMm = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + const blowoutOffsetFromTopMm = + blowoutLocation != null + ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP + : DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP const aspirateAirGapVolume = getAirGapData( fields, 'aspirate_airGap_checkbox', @@ -211,6 +219,10 @@ export const moveLiquidFormToArgs = ( name: hydratedFormData.stepName, dropTipLocation, nozzles, + aspirateXOffset: aspirate_x_position ?? 0, + aspirateYOffset: aspirate_y_position ?? 0, + dispenseXOffset: dispense_x_position ?? 0, + dispenseYOffset: dispense_y_position ?? 0, } console.assert( sourceWellsUnordered.length > 0, diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index 84803e31a745..081d78095668 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -59,13 +59,17 @@ describe('getDefaultsForStepType', () => { aspirate_delay_checkbox: false, aspirate_delay_mmFromBottom: null, aspirate_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, - + aspirate_x_position: 0, + aspirate_y_position: 0, dispense_airGap_checkbox: false, dispense_airGap_volume: null, dispense_delay_checkbox: false, dispense_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, dispense_delay_mmFromBottom: null, tipRack: null, + dispense_x_position: 0, + dispense_y_position: 0, + blowout_z_offset: 0, }) }) }) @@ -94,6 +98,9 @@ describe('getDefaultsForStepType', () => { aspirate_flowRate: null, dispense_flowRate: null, tipRack: null, + mix_x_position: 0, + mix_y_position: 0, + blowout_z_offset: 0, }) }) }) diff --git a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts index d441007b206e..16b1c5030f36 100644 --- a/protocol-designer/src/steplist/formLevel/test/warnings.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/warnings.test.ts @@ -1,11 +1,16 @@ import { describe, it, beforeEach, expect } from 'vitest' -import { fixture_24_tuberack } from '@opentrons/shared-data/labware/fixtures/2' +import { fixture24Tuberack, fixture96Plate } from '@opentrons/shared-data' import { _minAirGapVolume, belowPipetteMinimumVolume, minDisposalVolume, maxDispenseWellVolume, + aspirateTipPositionInTube, + dispenseTipPositionInTube, + mixTipPositionInTube, } from '../warnings' +import type { LabwareEntity } from '@opentrons/step-generation' +import type { LabwareDefinition2 } from '@opentrons/shared-data' type CheckboxFields = 'aspirate_airGap_checkbox' | 'dispense_airGap_checkbox' type VolumeFields = 'aspirate_airGap_volume' | 'dispense_airGap_volume' @@ -16,11 +21,15 @@ describe('Min air gap volume', () => { const volumeField = `${aspDisp}_airGap_volume` as VolumeFields describe(`${aspOrDisp} -> air gap`, () => { - let pipette: { spec: { minVolume: number } } + let pipette: { spec: { liquids: { default: { minVolume: number } } } } beforeEach(() => { pipette = { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, } }) @@ -82,12 +91,18 @@ describe('Min air gap volume', () => { }) }) describe('Below pipette minimum volume', () => { - let fieldsWithPipette: { pipette: { spec: { minVolume: number } } } + let fieldsWithPipette: { + pipette: { spec: { liquids: { default: { minVolume: number } } } } + } beforeEach(() => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, } @@ -119,7 +134,7 @@ describe('Below pipette minimum volume', () => { }) describe('Below min disposal volume', () => { let fieldsWithPipette: { - pipette: { spec: { minVolume: number } } + pipette: { spec: { liquids: { default: { minVolume: number } } } } disposalVolume_checkbox: boolean disposalVolume_volume: number path: string @@ -128,7 +143,11 @@ describe('Below min disposal volume', () => { fieldsWithPipette = { pipette: { spec: { - minVolume: 100, + liquids: { + default: { + minVolume: 100, + }, + }, }, }, disposalVolume_checkbox: true, @@ -201,7 +220,7 @@ describe('Max dispense well volume', () => { let fieldsWithDispenseLabware: any beforeEach(() => { fieldsWithDispenseLabware = { - dispense_labware: { def: { ...fixture_24_tuberack } }, + dispense_labware: { def: fixture24Tuberack }, dispense_wells: ['A1', 'A2'], } }) @@ -244,4 +263,75 @@ describe('Max dispense well volume', () => { // @ts-expect-error(sa, 2021-6-15): maxDispenseWellVolume might return null, need to null check before property access expect(maxDispenseWellVolume(fields).type).toBe('OVER_MAX_WELL_VOLUME') }) + describe('tip position in tube warnings', () => { + let fields: { + aspirate_labware: LabwareEntity + aspirate_mmFromBottom: number | null + labware: LabwareEntity + mix_mmFromBottom: number + dispense_labware: LabwareEntity + dispense_mmFromBottom: number | null + } + beforeEach(() => { + fields = { + aspirate_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + aspirate_mmFromBottom: null, + labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + mix_mmFromBottom: 0.5, + dispense_labware: { + def: fixture24Tuberack as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + }, + dispense_mmFromBottom: null, + } + }) + it('renders the errors for all 3', () => { + expect(aspirateTipPositionInTube(fields)?.type).toBe( + 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(dispenseTipPositionInTube(fields)?.type).toBe( + 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' + ) + expect(mixTipPositionInTube(fields)?.type).toBe( + 'MIX_TIP_POSITIONED_LOW_IN_TUBE' + ) + }) + it('renders null for all 3 when the number has been adjusted', () => { + fields.aspirate_mmFromBottom = 3 + fields.dispense_mmFromBottom = 3 + fields.mix_mmFromBottom = 3 + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + it('renders null for all 3 when the labware is not a tube rack', () => { + fields.aspirate_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + fields.dispense_labware = { + def: fixture96Plate as LabwareDefinition2, + id: 'mockId', + labwareDefURI: 'mockURI', + } + expect(aspirateTipPositionInTube(fields)).toBe(null) + expect(dispenseTipPositionInTube(fields)).toBe(null) + expect(mixTipPositionInTube(fields)).toBe(null) + }) + }) }) diff --git a/protocol-designer/src/steplist/formLevel/warnings.tsx b/protocol-designer/src/steplist/formLevel/warnings.tsx index 1b6fa0ab071f..6a9c31a1a725 100644 --- a/protocol-designer/src/steplist/formLevel/warnings.tsx +++ b/protocol-designer/src/steplist/formLevel/warnings.tsx @@ -1,16 +1,19 @@ import * as React from 'react' import { getWellTotalVolume } from '@opentrons/shared-data' import { KnowledgeBaseLink } from '../../components/KnowledgeBaseLink' -import { FormError } from './errors' +import type { FormError } from './errors' /******************* ** Warning Messages ** ********************/ export type FormWarningType = + | 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE' + | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'BELOW_MIN_DISPOSAL_VOLUME' | 'BELOW_PIPETTE_MINIMUM_VOLUME' + | 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE' | 'OVER_MAX_WELL_VOLUME' - | 'BELOW_MIN_DISPOSAL_VOLUME' - | 'BELOW_MIN_AIR_GAP_VOLUME' + | 'MIX_TIP_POSITIONED_LOW_IN_TUBE' export type FormWarning = FormError & { type: FormWarningType @@ -56,6 +59,27 @@ const belowMinDisposalVolumeWarning = (min: number): FormWarning => ({ dependentFields: ['disposalVolume_volume', 'pipette'], }) +const aspirateTipPositionedLowInTube = (): FormWarning => ({ + type: 'ASPIRATE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default aspirate height is 1mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['aspirate_labware'], +}) + +const dispenseTipPositionedLowInTube = (): FormWarning => ({ + type: 'DISPENSE_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default dispense height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['dispense_labware'], +}) + +const mixTipPositionedLowInTube = (): FormWarning => ({ + type: 'MIX_TIP_POSITIONED_LOW_IN_TUBE', + title: + 'The default mix height is 0.5mm from the bottom of the well, which could cause liquid overflow or pipette damage. Edit tip position in advanced settings.', + dependentFields: ['labware'], +}) + export type WarningChecker = (val: unknown) => FormWarning | null /******************* @@ -64,14 +88,57 @@ export type WarningChecker = (val: unknown) => FormWarning | null // TODO: real HydratedFormData type export type HydratedFormData = any +export const aspirateTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { aspirate_labware, aspirate_mmFromBottom } = fields + let isTubeRack: boolean = false + if (aspirate_labware != null) { + isTubeRack = aspirate_labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && aspirate_mmFromBottom === null + ? aspirateTipPositionedLowInTube() + : null +} +export const dispenseTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { dispense_labware, dispense_mmFromBottom } = fields + let isTubeRack: boolean = false + if (dispense_labware != null) { + isTubeRack = + // checking that the dispense labware is a labware and not a trash/waste chute + 'def' in dispense_labware + ? dispense_labware.def.metadata.displayCategory === 'tubeRack' + : false + } + return isTubeRack && dispense_mmFromBottom === null + ? dispenseTipPositionedLowInTube() + : null +} +export const mixTipPositionInTube = ( + fields: HydratedFormData +): FormWarning | null => { + const { labware, mix_mmFromBottom } = fields + let isTubeRack: boolean = false + if (labware != null) { + isTubeRack = labware.def.metadata.displayCategory === 'tubeRack' + } + return isTubeRack && mix_mmFromBottom === 0.5 + ? mixTipPositionedLowInTube() + : null +} export const belowPipetteMinimumVolume = ( fields: HydratedFormData ): FormWarning | null => { const { pipette, volume } = fields if (!(pipette && pipette.spec)) return null - return volume < pipette.spec.minVolume - ? belowPipetteMinVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + return volume < minVolume ? belowPipetteMinVolumeWarning(minVolume) : null } export const maxDispenseWellVolume = ( @@ -102,11 +169,16 @@ export const minDisposalVolume = ( } = fields if (!(pipette && pipette.spec) || path !== 'multiDispense') return null const isUnselected = !disposalVolume_checkbox || !disposalVolume_volume - if (isUnselected) return belowMinDisposalVolumeWarning(pipette.spec.minVolume) - const isBelowMin = disposalVolume_volume < pipette.spec.minVolume - return isBelowMin - ? belowMinDisposalVolumeWarning(pipette.spec.minVolume) - : null + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + if (isUnselected) { + return belowMinDisposalVolumeWarning(minVolume) + } + const isBelowMin = disposalVolume_volume < minVolume + return isBelowMin ? belowMinDisposalVolumeWarning(minVolume) : null } // both aspirate and dispense air gap volumes have the same minimums @@ -117,10 +189,16 @@ export const _minAirGapVolume = ( const checkboxValue = fields[checkboxField] const volumeValue = fields[volumeField] const { pipette } = fields - if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) return null - - const isBelowMin = Number(volumeValue) < pipette.spec.minVolume - return isBelowMin ? belowMinAirGapVolumeWarning(pipette.spec.minVolume) : null + if (!checkboxValue || !volumeValue || !pipette || !pipette.spec) { + return null + } + const liquidSpecs = pipette.spec.liquids + const minVolume = + 'lowVolumeDefault' in liquidSpecs + ? liquidSpecs.lowVolumeDefault.minVolume + : liquidSpecs.default.minVolume + const isBelowMin = Number(volumeValue) < minVolume + return isBelowMin ? belowMinAirGapVolumeWarning(minVolume) : null } export const minAspirateAirGapVolume: ( diff --git a/protocol-designer/src/steplist/generateSubstepItem.ts b/protocol-designer/src/steplist/generateSubstepItem.ts index f16b48f412cd..edfac2fd19eb 100644 --- a/protocol-designer/src/steplist/generateSubstepItem.ts +++ b/protocol-designer/src/steplist/generateSubstepItem.ts @@ -411,6 +411,7 @@ export function generateSubstepItem( temperature: temperature, labwareNickname: labwareNames?.nickname, message: stepArgs.message, + moduleId: stepArgs.module, } } diff --git a/protocol-designer/src/steplist/test/generateSubsteps.test.ts b/protocol-designer/src/steplist/test/generateSubsteps.test.ts index df8c3f5c3343..1c2483e04879 100644 --- a/protocol-designer/src/steplist/test/generateSubsteps.test.ts +++ b/protocol-designer/src/steplist/test/generateSubsteps.test.ts @@ -622,6 +622,7 @@ describe('generateSubstepItem', () => { temperature: 45, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -652,6 +653,7 @@ describe('generateSubstepItem', () => { temperature: 0, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) @@ -680,6 +682,7 @@ describe('generateSubstepItem', () => { temperature: null, labwareNickname: 'temp nickname', message: null, + moduleId: 'tempId', }) }) diff --git a/protocol-designer/src/steplist/types.ts b/protocol-designer/src/steplist/types.ts index 273fe87afdcd..297c13e71946 100644 --- a/protocol-designer/src/steplist/types.ts +++ b/protocol-designer/src/steplist/types.ts @@ -5,9 +5,9 @@ import { PauseArgs, ThermocyclerProfileStepArgs, } from '@opentrons/step-generation' -import { ModuleType } from '@opentrons/shared-data' -import { StepIdType } from '../form-types' -import { FormError } from './formLevel/errors' +import type { ModuleType } from '@opentrons/shared-data' +import type { StepIdType } from '../form-types' +import type { FormError } from './formLevel/errors' // timeline start and end export const START_TERMINAL_ITEM_ID: '__initial_setup__' = '__initial_setup__' export const END_TERMINAL_ITEM_ID: '__end__' = '__end__' @@ -105,6 +105,7 @@ export interface TemperatureSubstepItem { substepType: 'temperature' temperature: number | null labwareNickname: string | null | undefined + moduleId: string | null message?: string } export interface PauseSubstepItem { diff --git a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts index ede96f0be525..1717dc838cb5 100644 --- a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts +++ b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts @@ -50,6 +50,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, b: { @@ -86,6 +90,10 @@ describe('generateRobotStateTimeline', () => { description: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, c: { @@ -114,6 +122,10 @@ describe('generateRobotStateTimeline', () => { dispenseDelaySeconds: null, nozzles: null, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + aspirateYOffset: 0, + dispenseXOffset: 0, + dispenseYOffset: 0, }, }, } diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 9396bd121b8d..6c66367fb4ff 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -11,6 +11,7 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, isAddressableAreaStandardSlot, MOVABLE_TRASH_ADDRESSABLE_AREAS, + FLEX_MODULE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { @@ -232,7 +233,8 @@ export const getUnoccupiedLabwareLocationOptions: Selector< .includes(slotId) && !isTrashSlot && !WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) && - !notSelectedStagingAreaAddressableAreas.includes(slotId) + !notSelectedStagingAreaAddressableAreas.includes(slotId) && + !FLEX_MODULE_ADDRESSABLE_AREAS.includes(slotId) ) }) .map(slotId => ({ name: slotId, value: slotId })) diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index a0eee9ffff31..6d82f7832c94 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -2,6 +2,7 @@ import * as actions from './actions' import { rootReducer, RootState } from './reducers' import * as selectors from './selectors' type HintKey = // normal hints + | 'multiple_modules_without_labware' | 'add_liquids_and_labware' | 'deck_setup_explanation' | 'module_without_labware' @@ -10,7 +11,7 @@ type HintKey = // normal hints | 'waste_chute_warning' // blocking hints | 'custom_labware_with_modules' - | 'export_v8_protocol_7_1' + | 'export_v8_1_protocol_7_3' | 'change_magnet_module_model' // DEPRECATED HINTS (keep a record to avoid name collisions with old persisted dismissal states) // 'export_v4_protocol' @@ -19,5 +20,6 @@ type HintKey = // normal hints // | 'export_v6_protocol_6_10' // | 'export_v6_protocol_6_20' // | 'export_v7_protocol_7_0' +// | 'export_v8_protocol_7_1' export { actions, rootReducer, selectors } export type { RootState, HintKey } diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index dd4be8f0c622..27b3ea9f3ae7 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -241,17 +241,22 @@ export const getDisposalOptions = createSelector( } ) -export const getTiprackOptions: Selector = createSelector( +export interface TiprackOption { + name: string + value: string + defURI: string +} +export const getTiprackOptions: Selector = createSelector( stepFormSelectors.getLabwareEntities, getLabwareNicknamesById, (labwareEntities, nicknamesById) => { const options = reduce( labwareEntities, ( - acc: Options, + acc: TiprackOption[], labwareEntity: LabwareEntity, labwareId: string - ): Options => { + ): TiprackOption[] => { const labwareDefURI = labwareEntity.labwareDefURI const optionValues = acc.map(option => option.value) @@ -266,12 +271,13 @@ export const getTiprackOptions: Selector = createSelector( { name: nicknamesById[labwareId], value: labwareId, + defURI: labwareDefURI, }, ] } }, [] ) - return _sortLabwareDropdownOptions(options) + return options } ) diff --git a/protocol-designer/src/ui/modules/selectors.ts b/protocol-designer/src/ui/modules/selectors.ts index 75057c88dfa2..1d5ec7bdb08c 100644 --- a/protocol-designer/src/ui/modules/selectors.ts +++ b/protocol-designer/src/ui/modules/selectors.ts @@ -1,4 +1,5 @@ import { createSelector } from 'reselect' +import mapValues from 'lodash/mapValues' import { getLabwareDisplayName, MAGNETIC_MODULE_TYPE, @@ -6,7 +7,6 @@ import { THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, } from '@opentrons/shared-data' -import mapValues from 'lodash/mapValues' import { getInitialDeckSetup } from '../../step-forms/selectors' import { getLabwareNicknamesById } from '../labware/selectors' import { @@ -15,10 +15,14 @@ import { getModuleOnDeckByType, getModuleHasLabware, getMagnetLabwareEngageHeight as getMagnetLabwareEngageHeightUtil, + getModulesOnDeckByType, + getModulesHaveLabware, + ModuleAndLabware, } from './utils' -import { Options } from '@opentrons/components' -import { Selector } from '../../types' -import { LabwareNamesByModuleId } from '../../steplist/types' +import type { Options } from '@opentrons/components' +import type { Selector } from '../../types' +import type { LabwareNamesByModuleId } from '../../steplist/types' + export const getLabwareNamesByModuleId: Selector = createSelector( getInitialDeckSetup, getLabwareNicknamesById, @@ -84,16 +88,18 @@ export const getSingleMagneticModuleId: Selector< getModuleOnDeckByType(initialDeckSetup, MAGNETIC_MODULE_TYPE)?.id || null ) -/** Get single temperature module (assumes no multiples) */ -export const getSingleTemperatureModuleId: Selector< - string | null +/** Get all temperature modules */ +export const getTemperatureModuleIds: Selector< + string[] | null > = createSelector( getInitialDeckSetup, initialDeckSetup => - getModuleOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.id || null + getModulesOnDeckByType(initialDeckSetup, TEMPERATURE_MODULE_TYPE)?.map( + module => module.id + ) || null ) -/** Get single temperature module (assumes no multiples) */ +/** Get single thermocycler module (assumes no multiples) */ export const getSingleThermocyclerModuleId: Selector< string | null > = createSelector( @@ -111,13 +117,12 @@ export const getMagnetModuleHasLabware: Selector = createSelector( } ) -/** Returns boolean if temperature module has labware */ -export const getTemperatureModuleHasLabware: Selector = createSelector( - getInitialDeckSetup, - initialDeckSetup => { - return getModuleHasLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) - } -) +/** Returns all moduleIds and if they have labware for MoaM */ +export const getTemperatureModulesHaveLabware: Selector< + ModuleAndLabware[] +> = createSelector(getInitialDeckSetup, initialDeckSetup => { + return getModulesHaveLabware(initialDeckSetup, TEMPERATURE_MODULE_TYPE) +}) /** Returns boolean if thermocycler module has labware */ export const getThermocyclerModuleHasLabware: Selector = createSelector( diff --git a/protocol-designer/src/ui/modules/utils.ts b/protocol-designer/src/ui/modules/utils.ts index fcd1ddb5f434..e49e8ad7b338 100644 --- a/protocol-designer/src/ui/modules/utils.ts +++ b/protocol-designer/src/ui/modules/utils.ts @@ -20,12 +20,25 @@ export function getModuleOnDeckByType( (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type ) } +export function getModulesOnDeckByType( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleOnDeck[] | null | undefined { + return values(initialDeckSetup.modules).filter( + (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.type === type + ) +} export function getLabwareOnModule( initialDeckSetup: InitialDeckSetup, moduleId: string ): LabwareOnDeck | null | undefined { return values(initialDeckSetup.labware).find( - (lab: LabwareOnDeck) => lab.slot === moduleId + (labware: LabwareOnDeck) => + labware.slot === moduleId || + // acccount for adapter! + values(initialDeckSetup.labware).find( + adapter => adapter.id === labware.slot && adapter.slot === moduleId + ) ) } export function getModuleUnderLabware( @@ -81,28 +94,39 @@ export function getModuleLabwareOptions( nicknamesById: Record, type: ModuleType ): Options { - const moduleOnDeck = getModuleOnDeckByType(initialDeckSetup, type) - const labware = - moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + const labwares = initialDeckSetup.labware + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) const module = getModuleShortNames(type) let options: Options = [] - if (moduleOnDeck) { - if (labware) { - options = [ - { - name: `${nicknamesById[labware.id]} in ${module}`, + if (modulesOnDeck != null) { + options = modulesOnDeck.map(moduleOnDeck => { + const labware = getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) + if (labware) { + const labwareOnAdapterId = + labwares[labware.id] != null ? labwares[labware.id].id : null + if (labwareOnAdapterId != null) { + return { + name: `${nicknamesById[labwareOnAdapterId]} in ${ + nicknamesById[labware.id] + } in ${module} in slot ${moduleOnDeck.slot}`, + value: moduleOnDeck.id, + } + } else { + return { + name: `${nicknamesById[labware.id]} in ${module} in slot ${ + moduleOnDeck.slot + }`, + value: moduleOnDeck.id, + } + } + } else { + return { + name: `No labware in ${module} in slot ${moduleOnDeck.slot}`, value: moduleOnDeck.id, - }, - ] - } else { - options = [ - { - name: `${module} No labware on module`, - value: moduleOnDeck.id, - }, - ] - } + } + } + }) } return options @@ -116,6 +140,29 @@ export function getModuleHasLabware( moduleOnDeck && getLabwareOnModule(initialDeckSetup, moduleOnDeck.id) return Boolean(moduleOnDeck) && Boolean(labware) } + +export interface ModuleAndLabware { + moduleId: string + hasLabware: boolean +} + +export function getModulesHaveLabware( + initialDeckSetup: InitialDeckSetup, + type: ModuleType +): ModuleAndLabware[] { + const modulesOnDeck = getModulesOnDeckByType(initialDeckSetup, type) + const moduleAndLabware: ModuleAndLabware[] = [] + modulesOnDeck?.forEach(module => { + const labwareHasModule = getLabwareOnModule(initialDeckSetup, module.id) + + moduleAndLabware.push({ + moduleId: module.id, + hasLabware: labwareHasModule != null, + }) + }) + return moduleAndLabware +} + export const getMagnetLabwareEngageHeight = ( initialDeckSetup: InitialDeckSetup, magnetModuleId: string | null diff --git a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts index 2a087d4ac310..56046da6a98b 100644 --- a/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts +++ b/protocol-designer/src/ui/steps/actions/__tests__/addAndSelectStepWithHints.test.ts @@ -19,15 +19,13 @@ beforeEach(() => { vi.mocked(addHint).mockReturnValue('addHintReturnValue' as any) vi.mocked(labwareIngredSelectors.getDeckHasLiquid).mockReturnValue(true) vi.mocked(uiModuleSelectors.getMagnetModuleHasLabware).mockReturnValue(false) - vi.mocked(uiModuleSelectors.getTemperatureModuleHasLabware).mockReturnValue( - false + vi.mocked(uiModuleSelectors.getTemperatureModulesHaveLabware).mockReturnValue( + [] ) vi.mocked(uiModuleSelectors.getThermocyclerModuleHasLabware).mockReturnValue( false ) - vi.mocked(uiModuleSelectors.getSingleTemperatureModuleId).mockReturnValue( - null - ) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue(null) vi.mocked(uiModuleSelectors.getSingleThermocyclerModuleId).mockReturnValue( null ) @@ -89,10 +87,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'magnet' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: [], }, }, { @@ -100,10 +99,13 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + ], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: 'something', getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId'], }, }, { @@ -111,10 +113,11 @@ describe('addAndSelectStepWithHints', () => { stepType: 'temperature' as StepType, selectorValues: { getMagnetModuleHasLabware: false, - getTemperatureModuleHasLabware: false, + getTemperatureModulesHaveLabware: [], getThermocyclerModuleHasLabware: false, getSingleTemperatureModuleId: null, getSingleThermocyclerModuleId: 'something', + getTemperatureModuleIds: [], }, }, ].forEach(({ testName, stepType, selectorValues }) => { @@ -123,14 +126,14 @@ describe('addAndSelectStepWithHints', () => { selectorValues.getMagnetModuleHasLabware ) vi.mocked( - uiModuleSelectors.getTemperatureModuleHasLabware - ).mockReturnValue(selectorValues.getTemperatureModuleHasLabware) + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) vi.mocked( uiModuleSelectors.getThermocyclerModuleHasLabware ).mockReturnValue(selectorValues.getThermocyclerModuleHasLabware) - vi.mocked( - uiModuleSelectors.getSingleTemperatureModuleId - ).mockReturnValue(selectorValues.getSingleTemperatureModuleId) + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) vi.mocked( uiModuleSelectors.getSingleThermocyclerModuleId ).mockReturnValue(selectorValues.getSingleThermocyclerModuleId) @@ -159,4 +162,56 @@ describe('addAndSelectStepWithHints', () => { }) }) }) + describe('ADD_HINT "multiple_modules_without_labware"', () => { + ;[ + { + testName: 'temperature step, when temperature module has no labware', + stepType: 'temperature' as StepType, + selectorValues: { + getMagnetModuleHasLabware: false, + getTemperatureModulesHaveLabware: [ + { moduleId: 'mockId', hasLabware: false }, + { moduleId: 'mockId2', hasLabware: true }, + ], + getThermocyclerModuleHasLabware: false, + getSingleTemperatureModuleId: 'something', + getSingleThermocyclerModuleId: null, + getTemperatureModuleIds: ['mockId', 'mockId2'], + }, + }, + ].forEach(({ testName, stepType, selectorValues }) => { + it(`should be dispatched (after addStep thunk is dispatched) for ${testName}`, () => { + vi.mocked( + uiModuleSelectors.getTemperatureModulesHaveLabware + ).mockReturnValue(selectorValues.getTemperatureModulesHaveLabware) + + vi.mocked(uiModuleSelectors.getTemperatureModuleIds).mockReturnValue( + selectorValues.getTemperatureModuleIds + ) + + const payload = { + stepType, + } + addAndSelectStepWithHints(payload)(dispatch, getState) + expect(vi.mocked(addHint).mock.calls).toEqual([ + ['multiple_modules_without_labware'], + ]) + expect(dispatch.mock.calls).toEqual([ + [ + { + type: 'ADD_STEP', + payload: { + id: PRESAVED_STEP_ID, + stepType, + }, + meta: { + robotStateTimeline: 'mockGetRobotStateTimelineValue', + }, + }, + ], + ['addHintReturnValue'], + ]) + }) + }) + }) }) diff --git a/protocol-designer/src/ui/steps/actions/thunks/index.ts b/protocol-designer/src/ui/steps/actions/thunks/index.ts index c6d8be201594..9cc31de8ab8f 100644 --- a/protocol-designer/src/ui/steps/actions/thunks/index.ts +++ b/protocol-designer/src/ui/steps/actions/thunks/index.ts @@ -40,18 +40,21 @@ export const addAndSelectStepWithHints: (arg: { const magnetModuleHasLabware = uiModuleSelectors.getMagnetModuleHasLabware( state ) - const temperatureModuleHasLabware = uiModuleSelectors.getTemperatureModuleHasLabware( + const temperatureModulesHaveLabware = uiModuleSelectors.getTemperatureModulesHaveLabware( state ) const thermocyclerModuleHasLabware = uiModuleSelectors.getThermocyclerModuleHasLabware( state ) - const temperatureModuleOnDeck = uiModuleSelectors.getSingleTemperatureModuleId( + const temperatureModuleOnDeck = uiModuleSelectors.getTemperatureModuleIds( state ) const thermocyclerModuleOnDeck = uiModuleSelectors.getSingleThermocyclerModuleId( state ) + const tempHasNoLabware = temperatureModulesHaveLabware.some( + module => !module.hasLabware + ) // TODO: Ian 2019-01-17 move out to centralized step info file - see #2926 const stepNeedsLiquid = ['mix', 'moveLiquid'].includes(payload.stepType) const stepMagnetNeedsLabware = ['magnet'].includes(payload.stepType) @@ -59,15 +62,17 @@ export const addAndSelectStepWithHints: (arg: { const stepModuleMissingLabware = (stepMagnetNeedsLabware && !magnetModuleHasLabware) || (stepTemperatureNeedsLabware && - ((temperatureModuleOnDeck && !temperatureModuleHasLabware) || - (thermocyclerModuleOnDeck && !thermocyclerModuleHasLabware))) + thermocyclerModuleOnDeck && + !thermocyclerModuleHasLabware) || + (temperatureModuleOnDeck?.length === 1 && tempHasNoLabware) if (stepNeedsLiquid && !deckHasLiquid) { dispatch(tutorialActions.addHint('add_liquids_and_labware')) } - if (stepModuleMissingLabware) { dispatch(tutorialActions.addHint('module_without_labware')) + } else if (temperatureModuleOnDeck && tempHasNoLabware) { + dispatch(tutorialActions.addHint('multiple_modules_without_labware')) } } export interface ReorderSelectedStepAction { diff --git a/protocol-designer/src/ui/steps/selectors.ts b/protocol-designer/src/ui/steps/selectors.ts index f9a228366d3d..8ed2eeb20ddc 100644 --- a/protocol-designer/src/ui/steps/selectors.ts +++ b/protocol-designer/src/ui/steps/selectors.ts @@ -136,10 +136,11 @@ export const getHoveredStepLabware = createSelector( // only 1 labware return [stepArgs.labware] } - // @ts-expect-error(sa, 2021-6-15): type narrow stepArgs.module - if (stepArgs.module) { - // @ts-expect-error(sa, 2021-6-15): this expect error should not be necessary after type narrowing above - const labware = getLabwareOnModule(initialDeckState, stepArgs.module) + if ('module' in stepArgs) { + const labware = getLabwareOnModule( + initialDeckState, + stepArgs.module ?? '' + ) return labware ? [labware.id] : [] } @@ -150,8 +151,9 @@ export const getHoveredStepLabware = createSelector( // step types that have no labware that gets highlighted if (!(stepArgs.commandCreatorFnName === 'delay')) { - // TODO Ian 2018-05-08 use assert here console.warn( + // @ts-expect-error: should only reach this warning when new step is added and + // highlighted wells is not yet implemented `getHoveredStepLabware does not support step type "${stepArgs.commandCreatorFnName}"` ) } diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 5cf64a591604..e5aa13d10c58 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -418,10 +418,26 @@ describe('_getSavedMultiSelectFieldValues', () => { isIndeterminate: false, value: undefined, }, + aspirate_labware: { value: 'aspirate_labware_id', isIndeterminate: false, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, + blowout_z_offset: { + isIndeterminate: false, + }, aspirate_wells: { isIndeterminate: true, }, @@ -669,6 +685,21 @@ describe('_getSavedMultiSelectFieldValues', () => { path: { isIndeterminate: true, }, + aspirate_x_position: { + isIndeterminate: false, + }, + aspirate_y_position: { + isIndeterminate: false, + }, + dispense_x_position: { + isIndeterminate: false, + }, + dispense_y_position: { + isIndeterminate: false, + }, + blowout_z_offset: { + isIndeterminate: false, + }, preWetTip: { isIndeterminate: true, }, @@ -850,6 +881,15 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { value: false, isIndeterminate: false }, mix_touchTip_mmFromBottom: { value: null, isIndeterminate: false }, nozzles: { value: undefined, isIndeterminate: false }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -920,6 +960,15 @@ describe('_getSavedMultiSelectFieldValues', () => { mix_touchTip_checkbox: { isIndeterminate: true }, mix_touchTip_mmFromBottom: { isIndeterminate: true }, nozzles: { isIndeterminate: true }, + mix_x_position: { + isIndeterminate: false, + }, + mix_y_position: { + isIndeterminate: false, + }, + blowout_z_offset: { + isIndeterminate: false, + }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 40fb567b62b0..3c5acc812486 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -1,12 +1,13 @@ import path from 'path' -import { UserConfig, defineConfig } from 'vite' +import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' -import { versionForProject } from '../scripts/git-version' +import { versionForProject } from '../scripts/git-version.mjs' +import type { UserConfig } from 'vite' const testAliases: {} | { 'file-saver': string } = process.env.CYPRESS === '1' diff --git a/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx new file mode 100644 index 000000000000..e04c020fb1da --- /dev/null +++ b/react-api-client/src/protocols/__tests__/useCreateProtocolAnalysisMutation.test.tsx @@ -0,0 +1,77 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { act, renderHook, waitFor } from '@testing-library/react' +import { createProtocolAnalysis } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useCreateProtocolAnalysisMutation } from '..' +import type { HostConfig, Response } from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const ANALYSIS_SUMMARY_RESPONSE = [ + { id: 'fakeAnalysis1', status: 'completed' }, + { id: 'fakeAnalysis2', status: 'pending' }, +] as ProtocolAnalysisSummary[] + +describe('useCreateProtocolAnalysisMutation hook', () => { + let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent<{ + children: React.ReactNode + }> = ({ children }) => ( + {children} + ) + wrapper = clientProvider + }) + + it('should return no data when calling createProtocolAnalysis if the request fails', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockRejectedValue('oh no') + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + + expect(result.current.data).toBeUndefined() + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + await waitFor(() => { + expect(result.current.data).toBeUndefined() + }) + }) + + it('should create an array of ProtocolAnalysisSummaries when calling the createProtocolAnalysis callback', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(createProtocolAnalysis).mockResolvedValue({ + data: ANALYSIS_SUMMARY_RESPONSE, + } as Response) + + const { result } = renderHook( + () => useCreateProtocolAnalysisMutation('fake-protocol-key'), + { + wrapper, + } + ) + act(() => + result.current.createProtocolAnalysis({ + protocolKey: 'fake-protocol-key', + runTimeParameterValues: {}, + }) + ) + + await waitFor(() => { + expect(result.current.data).toEqual(ANALYSIS_SUMMARY_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/protocols/__tests__/useCreateProtocolMutation.test.tsx b/react-api-client/src/protocols/__tests__/useCreateProtocolMutation.test.tsx index f6192eb8ec06..9872410db238 100644 --- a/react-api-client/src/protocols/__tests__/useCreateProtocolMutation.test.tsx +++ b/react-api-client/src/protocols/__tests__/useCreateProtocolMutation.test.tsx @@ -95,6 +95,7 @@ describe('useCreateProtocolMutation hook', () => { result.current.createProtocol({ files: createProtocolData, protocolKey: 'fakeProtocolKey', + runTimeParameterValues: { fakeParamName: 5.0 }, }) ) diff --git a/react-api-client/src/protocols/index.ts b/react-api-client/src/protocols/index.ts index ddf7c3eeaacf..561dee01e8b2 100644 --- a/react-api-client/src/protocols/index.ts +++ b/react-api-client/src/protocols/index.ts @@ -4,4 +4,5 @@ export { useProtocolQuery } from './useProtocolQuery' export { useProtocolAnalysesQuery } from './useProtocolAnalysesQuery' export { useProtocolAnalysisAsDocumentQuery } from './useProtocolAnalysisAsDocumentQuery' export { useCreateProtocolMutation } from './useCreateProtocolMutation' +export { useCreateProtocolAnalysisMutation } from './useCreateProtocolAnalysisMutation' export { useDeleteProtocolMutation } from './useDeleteProtocolMutation' diff --git a/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts new file mode 100644 index 000000000000..f8ba6e105868 --- /dev/null +++ b/react-api-client/src/protocols/useCreateProtocolAnalysisMutation.ts @@ -0,0 +1,86 @@ +import { createProtocolAnalysis } from '@opentrons/api-client' +import { useMutation, useQueryClient } from 'react-query' +import { useHost } from '../api' +import type { + ErrorResponse, + HostConfig, + RunTimeParameterCreateData, +} from '@opentrons/api-client' +import type { ProtocolAnalysisSummary } from '@opentrons/shared-data' +import type { AxiosError } from 'axios' +import type { + UseMutationResult, + UseMutationOptions, + UseMutateFunction, +} from 'react-query' + +export interface CreateProtocolAnalysisVariables { + protocolKey: string + runTimeParameterValues?: RunTimeParameterCreateData + forceReAnalyze?: boolean +} +export type UseCreateProtocolMutationResult = UseMutationResult< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> & { + createProtocolAnalysis: UseMutateFunction< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + > +} + +export type UseCreateProtocolAnalysisMutationOptions = UseMutationOptions< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables +> + +export function useCreateProtocolAnalysisMutation( + protocolId: string | null, + hostOverride?: HostConfig | null, + options: UseCreateProtocolAnalysisMutationOptions | undefined = {} +): UseCreateProtocolMutationResult { + const contextHost = useHost() + const host = + hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost + const queryClient = useQueryClient() + + const mutation = useMutation< + ProtocolAnalysisSummary[], + AxiosError, + CreateProtocolAnalysisVariables + >( + [host, 'protocols', protocolId, 'analyses'], + ({ protocolKey, runTimeParameterValues, forceReAnalyze }) => + createProtocolAnalysis( + host as HostConfig, + protocolKey, + runTimeParameterValues, + forceReAnalyze + ) + .then(response => { + queryClient + .invalidateQueries([host, 'protocols', protocolId, 'analyses']) + .then(() => + queryClient.setQueryData( + [host, 'protocols', protocolId, 'analyses'], + response.data + ) + ) + .catch((e: Error) => { + throw e + }) + return response.data + }) + .catch((e: Error) => { + throw e + }), + options + ) + return { + ...mutation, + createProtocolAnalysis: mutation.mutate, + } +} diff --git a/react-api-client/src/protocols/useCreateProtocolMutation.ts b/react-api-client/src/protocols/useCreateProtocolMutation.ts index 1474787b75e6..2e36321e3116 100644 --- a/react-api-client/src/protocols/useCreateProtocolMutation.ts +++ b/react-api-client/src/protocols/useCreateProtocolMutation.ts @@ -8,11 +8,17 @@ import { import { createProtocol } from '@opentrons/api-client' import { useHost } from '../api' import type { AxiosError } from 'axios' -import type { ErrorResponse, HostConfig, Protocol } from '@opentrons/api-client' +import type { + ErrorResponse, + HostConfig, + Protocol, + RunTimeParameterCreateData, +} from '@opentrons/api-client' export interface CreateProtocolVariables { files: File[] protocolKey?: string + runTimeParameterValues?: RunTimeParameterCreateData } export type UseCreateProtocolMutationResult = UseMutationResult< Protocol, @@ -34,7 +40,8 @@ export type UseCreateProtocolMutationOptions = UseMutationOptions< export function useCreateProtocolMutation( options: UseCreateProtocolMutationOptions = {}, - hostOverride?: HostConfig | null + hostOverride?: HostConfig | null, + runTimeParameterValues?: RunTimeParameterCreateData ): UseCreateProtocolMutationResult { const contextHost = useHost() const host = @@ -48,7 +55,12 @@ export function useCreateProtocolMutation( >( [host, 'protocols'], ({ files: protocolFiles, protocolKey }) => - createProtocol(host as HostConfig, protocolFiles, protocolKey) + createProtocol( + host as HostConfig, + protocolFiles, + protocolKey, + runTimeParameterValues + ) .then(response => { const protocolId = response.data.data.id queryClient diff --git a/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx b/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx new file mode 100644 index 000000000000..2f980be94734 --- /dev/null +++ b/react-api-client/src/robot/__tests__/useRobotSettingsQuery.test.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' +import { renderHook, waitFor } from '@testing-library/react' + +import { getRobotSettings } from '@opentrons/api-client' +import { useHost } from '../../api' +import { useRobotSettingsQuery } from '..' + +import type { + HostConfig, + Response, + RobotSettingsResponse, +} from '@opentrons/api-client' +import type { UseRobotSettingsQueryOptions } from '../useRobotSettingsQuery' + +vi.mock('@opentrons/api-client') +vi.mock('../../api/useHost') + +const HOST_CONFIG: HostConfig = { hostname: 'localhost' } +const ROBOT_SETTINGS_RESPONSE: RobotSettingsResponse = { + settings: [ + { + id: 'enableOEMMode', + title: 'Enable OEM Mode', + description: 'a mode for an OEM', + value: false, + }, + ], +} + +describe('useRobotSettingsQuery hook', () => { + let wrapper: React.FunctionComponent< + { children: React.ReactNode } & UseRobotSettingsQueryOptions + > + + beforeEach(() => { + const queryClient = new QueryClient() + const clientProvider: React.FunctionComponent< + { children: React.ReactNode } & UseRobotSettingsQueryOptions + > = ({ children }) => ( + {children} + ) + + wrapper = clientProvider + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it('should return no data if no host', () => { + vi.mocked(useHost).mockReturnValue(null) + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + expect(result.current?.data).toBeUndefined() + }) + + it('should return no data if robot settings request fails', () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getRobotSettings).mockRejectedValue('oh no') + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + expect(result.current?.data).toBeUndefined() + }) + + it('should return robot settings response data', async () => { + vi.mocked(useHost).mockReturnValue(HOST_CONFIG) + vi.mocked(getRobotSettings).mockResolvedValue({ + data: ROBOT_SETTINGS_RESPONSE, + } as Response) + + const { result } = renderHook(() => useRobotSettingsQuery(), { wrapper }) + + await waitFor(() => { + expect(result.current?.data).toEqual(ROBOT_SETTINGS_RESPONSE) + }) + }) +}) diff --git a/react-api-client/src/robot/index.ts b/react-api-client/src/robot/index.ts index 8a539abcea9d..4b296d6a4fe2 100644 --- a/react-api-client/src/robot/index.ts +++ b/react-api-client/src/robot/index.ts @@ -3,3 +3,4 @@ export { useEstopQuery } from './useEstopQuery' export { useLightsQuery } from './useLightsQuery' export { useAcknowledgeEstopDisengageMutation } from './useAcknowledgeEstopDisengageMutation' export { useSetLightsMutation } from './useSetLightsMutation' +export { useRobotSettingsQuery } from './useRobotSettingsQuery' diff --git a/react-api-client/src/robot/useRobotSettingsQuery.ts b/react-api-client/src/robot/useRobotSettingsQuery.ts new file mode 100644 index 000000000000..455457ec83b8 --- /dev/null +++ b/react-api-client/src/robot/useRobotSettingsQuery.ts @@ -0,0 +1,21 @@ +import { useQuery } from 'react-query' +import { getRobotSettings } from '@opentrons/api-client' +import { useHost } from '../api' + +import type { UseQueryResult, UseQueryOptions } from 'react-query' +import type { HostConfig, RobotSettingsResponse } from '@opentrons/api-client' + +export type UseRobotSettingsQueryOptions = UseQueryOptions + +export function useRobotSettingsQuery( + options: UseRobotSettingsQueryOptions = {} +): UseQueryResult { + const host = useHost() + const query = useQuery( + [host as HostConfig, 'robot_settings'], + () => getRobotSettings(host as HostConfig).then(response => response.data), + { enabled: host !== null, ...options } + ) + + return query +} diff --git a/robot-server/Pipfile b/robot-server/Pipfile index e6c1b7ba7945..2d22c6dc34c5 100755 --- a/robot-server/Pipfile +++ b/robot-server/Pipfile @@ -36,9 +36,11 @@ sqlalchemy2-stubs = "==0.0.2a21" # limited by tavern python-box = "==6.1.0" types-paho-mqtt = "==1.6.0.20240106" +performance-metrics = {file = "../performance-metrics", editable = true} [packages] anyio = "==3.7.1" +aiohttp = "==3.8.1" # fastapi >=0.100.0 is intended for use with pydantic 2.x, and while it theoretically is # backwards compatible, best to be sure fastapi = "==0.99.1" diff --git a/robot-server/Pipfile.lock b/robot-server/Pipfile.lock index e97832aab95d..2ea9f5456965 100644 --- a/robot-server/Pipfile.lock +++ b/robot-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d56512f7ae8f68fd80ec6eff41af08576468087a45578f5b2c8241e42d95b887" + "sha256": "9f64ba7d87b9c9fd510aac5c4a22fa748c1bb3b9936826ef2b4b13454c1c5e2b" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,85 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3", + "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782", + "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75", + "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf", + "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7", + "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675", + "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1", + "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785", + "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4", + "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf", + "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5", + "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15", + "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca", + "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8", + "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac", + "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8", + "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef", + "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516", + "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700", + "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2", + "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8", + "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0", + "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676", + "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad", + "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155", + "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db", + "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd", + "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091", + "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602", + "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411", + "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93", + "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd", + "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec", + "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51", + "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7", + "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17", + "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d", + "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00", + "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923", + "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440", + "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32", + "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e", + "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1", + "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724", + "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a", + "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8", + "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2", + "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33", + "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b", + "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2", + "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632", + "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b", + "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2", + "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316", + "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74", + "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96", + "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866", + "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44", + "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950", + "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa", + "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c", + "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a", + "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd", + "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd", + "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9", + "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421", + "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2", + "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922", + "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4", + "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237", + "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642", + "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==3.8.1" + }, "aionotify": { "hashes": [ "sha256:385e1becfaac2d9f4326673033d53912ef9565b6febdedbec593ee966df392c6", @@ -23,6 +102,14 @@ ], "version": "==0.2.0" }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, "anyio": { "hashes": [ "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", @@ -32,6 +119,14 @@ "markers": "python_version >= '3.7'", "version": "==3.7.1" }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.3" + }, "attrs": { "hashes": [ "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", @@ -40,6 +135,14 @@ "markers": "python_version >= '3.7'", "version": "==23.2.0" }, + "charset-normalizer": { + "hashes": [ + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" + }, "click": { "hashes": [ "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", @@ -66,6 +169,153 @@ "markers": "python_version >= '3.7'", "version": "==0.99.1" }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, + "greenlet": { + "hashes": [ + "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", + "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", + "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", + "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", + "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", + "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", + "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", + "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", + "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", + "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", + "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", + "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", + "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", + "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", + "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", + "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", + "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", + "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", + "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", + "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", + "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", + "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", + "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", + "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", + "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", + "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", + "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", + "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", + "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", + "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", + "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", + "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", + "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", + "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", + "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", + "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", + "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", + "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", + "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", + "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", + "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", + "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", + "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", + "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", + "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", + "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", + "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", + "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", + "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", + "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", + "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", + "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", + "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", + "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", + "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", + "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", + "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", + "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + ], + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==3.0.3" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -93,65 +343,161 @@ }, "msgpack": { "hashes": [ - "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", - "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d", - "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3", - "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", - "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0", - "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", - "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", - "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", - "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524", - "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819", - "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc", - "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc", - "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", - "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", - "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81", - "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", - "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", - "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2", - "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", - "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", - "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", - "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", - "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95", - "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f", - "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", - "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", - "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", - "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61", - "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", - "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", - "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d", - "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c", - "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", - "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", - "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", - "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", - "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", - "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", - "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", - "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f", - "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7", - "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", - "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", - "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", - "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf", - "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c", - "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", - "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", - "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", - "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", - "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", - "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad", - "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd", - "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7", - "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", - "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" ], "markers": "platform_system != 'Windows'", - "version": "==1.0.7" + "version": "==1.0.8" + }, + "multidict": { + "hashes": [ + "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", + "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", + "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", + "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", + "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", + "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", + "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", + "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", + "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", + "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", + "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", + "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", + "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", + "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", + "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", + "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", + "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", + "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", + "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", + "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", + "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", + "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", + "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", + "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", + "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", + "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", + "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", + "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", + "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", + "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", + "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", + "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", + "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", + "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", + "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", + "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", + "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", + "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", + "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", + "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", + "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", + "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", + "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", + "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", + "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", + "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", + "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", + "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", + "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", + "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", + "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", + "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", + "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", + "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", + "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", + "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", + "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", + "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", + "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", + "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", + "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", + "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", + "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", + "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", + "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", + "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", + "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", + "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", + "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", + "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", + "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", + "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", + "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", + "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", + "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", + "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", + "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", + "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", + "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", + "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", + "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", + "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", + "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", + "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", + "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", + "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", + "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", + "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", + "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", + "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.5" }, "numpy": { "hashes": [ @@ -182,6 +528,7 @@ }, "opentrons": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../api" }, "opentrons-hardware": { @@ -193,15 +540,16 @@ }, "opentrons-shared-data": { "editable": true, + "markers": "python_version >= '3.8'", "path": "../shared-data/python" }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "paho-mqtt": { "hashes": [ @@ -333,19 +681,19 @@ }, "setuptools": { "hashes": [ - "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05", - "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], "markers": "python_version >= '3.8'", - "version": "==69.0.3" + "version": "==69.5.1" }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "sqlalchemy": { "hashes": [ @@ -417,12 +765,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "uvicorn": { "hashes": [ @@ -518,6 +866,102 @@ "markers": "python_full_version >= '3.7.0'", "version": "==1.2.0" }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + }, "zipp": { "hashes": [ "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", @@ -594,11 +1038,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", + "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3'", - "version": "==2.0.12" + "markers": "python_full_version >= '3.6.0'", + "version": "==2.1.1" }, "click": { "hashes": [ @@ -622,61 +1066,61 @@ "toml" ], "hashes": [ - "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61", - "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1", - "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7", - "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7", - "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75", - "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd", - "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35", - "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04", - "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6", - "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042", - "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166", - "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1", - "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d", - "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c", - "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66", - "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70", - "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1", - "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676", - "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630", - "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a", - "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74", - "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad", - "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19", - "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6", - "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448", - "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018", - "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218", - "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756", - "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54", - "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45", - "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628", - "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968", - "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d", - "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25", - "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60", - "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950", - "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06", - "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295", - "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b", - "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c", - "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc", - "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74", - "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1", - "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee", - "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011", - "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156", - "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766", - "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5", - "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581", - "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016", - "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c", - "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3" + "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", + "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", + "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", + "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", + "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", + "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", + "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", + "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", + "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", + "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", + "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", + "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", + "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", + "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", + "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", + "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", + "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", + "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", + "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", + "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", + "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", + "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", + "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", + "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", + "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", + "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", + "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", + "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", + "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", + "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", + "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", + "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", + "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", + "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", + "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", + "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", + "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", + "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", + "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", + "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", + "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", + "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", + "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", + "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", + "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", + "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", + "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", + "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", + "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", + "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", + "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", + "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" ], "markers": "python_version >= '3.8'", - "version": "==7.4.1" + "version": "==7.4.4" }, "decoy": { "hashes": [ @@ -703,11 +1147,11 @@ }, "execnet": { "hashes": [ - "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41", - "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af" + "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", + "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.2" + "markers": "python_version >= '3.8'", + "version": "==2.1.1" }, "flake8": { "hashes": [ @@ -764,11 +1208,11 @@ }, "httpcore": { "hashes": [ - "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7", - "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535" + "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", + "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" ], "markers": "python_version >= '3.8'", - "version": "==1.0.2" + "version": "==1.0.5" }, "httpx": { "hashes": [ @@ -812,14 +1256,6 @@ "markers": "python_version >= '3.7'", "version": "==4.17.3" }, - "jsonschema-specifications": { - "hashes": [ - "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" - ], - "markers": "python_version >= '3.8'", - "version": "==2023.12.1" - }, "mccabe": { "hashes": [ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", @@ -879,13 +1315,18 @@ "markers": "python_version >= '3.5'", "version": "==1.0.0" }, + "opentrons-shared-data": { + "editable": true, + "markers": "python_version >= '3.8'", + "path": "../shared-data/python" + }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "paho-mqtt": { "hashes": [ @@ -909,6 +1350,10 @@ "markers": "python_version >= '2.6'", "version": "==6.0.0" }, + "performance-metrics": { + "editable": true, + "file": "../performance-metrics" + }, "platformdirs": { "hashes": [ "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", @@ -941,6 +1386,49 @@ "markers": "python_version >= '3.8'", "version": "==2.11.1" }, + "pydantic": { + "hashes": [ + "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", + "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", + "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", + "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", + "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", + "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", + "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", + "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", + "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", + "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", + "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", + "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", + "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", + "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", + "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", + "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", + "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", + "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", + "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", + "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", + "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", + "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", + "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", + "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", + "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", + "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", + "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", + "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", + "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", + "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", + "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", + "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", + "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", + "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", + "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", + "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.10.12" + }, "pydocstyle": { "hashes": [ "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", @@ -972,6 +1460,44 @@ ], "version": "==1.8.0" }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, "pytest": { "hashes": [ "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e", @@ -983,12 +1509,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:2143d9d9375bf372a73260e4114541485e84fca350b0b6b92674ca56ff5f7ea2", - "sha256:b0079dfac14b60cd1ce4691fbfb1748fe939db7d0234b5aba97197d10fbe0fef" + "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", + "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.4" + "version": "==0.23.6" }, "pytest-cov": { "hashes": [ @@ -1050,11 +1576,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -1113,14 +1639,6 @@ "markers": "python_version >= '3.6'", "version": "==6.0.1" }, - "referencing": { - "hashes": [ - "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5", - "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7" - ], - "markers": "python_version >= '3.8'", - "version": "==0.33.0" - }, "requests": { "hashes": [ "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", @@ -1130,118 +1648,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==2.27.1" }, - "rpds-py": { - "hashes": [ - "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147", - "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7", - "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2", - "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68", - "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1", - "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382", - "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d", - "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921", - "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38", - "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4", - "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a", - "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d", - "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518", - "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e", - "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d", - "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf", - "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5", - "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba", - "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6", - "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59", - "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253", - "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6", - "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f", - "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3", - "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea", - "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1", - "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76", - "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93", - "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad", - "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad", - "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc", - "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049", - "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d", - "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90", - "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d", - "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd", - "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25", - "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2", - "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f", - "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6", - "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4", - "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c", - "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8", - "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d", - "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b", - "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19", - "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453", - "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9", - "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde", - "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296", - "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58", - "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec", - "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99", - "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a", - "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb", - "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383", - "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d", - "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896", - "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc", - "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6", - "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b", - "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7", - "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22", - "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf", - "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394", - "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0", - "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57", - "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74", - "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83", - "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29", - "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9", - "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f", - "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745", - "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb", - "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811", - "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55", - "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342", - "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23", - "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82", - "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041", - "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb", - "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066", - "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55", - "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6", - "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a", - "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140", - "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b", - "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9", - "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256", - "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c", - "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772", - "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4", - "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae", - "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920", - "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a", - "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b", - "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361", - "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8", - "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.1" - }, "ruamel.yaml": { "hashes": [ - "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e", - "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada" + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" ], "markers": "python_version >= '3.7'", - "version": "==0.18.5" + "version": "==0.18.6" }, "ruamel.yaml.clib": { "hashes": [ @@ -1309,11 +1722,11 @@ }, "sniffio": { "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], "markers": "python_version >= '3.7'", - "version": "==1.3.0" + "version": "==1.3.1" }, "snowballstemmer": { "hashes": [ @@ -1358,12 +1771,12 @@ }, "types-mock": { "hashes": [ - "sha256:13ca379d5710ccb3f18f69ade5b08881874cb83383d8fb49b1d4dac9d5c5d090", - "sha256:3d116955495935b0bcba14954b38d97e507cd43eca3e3700fc1b8e4f5c6bf2c7" + "sha256:0769cb376dfc75b45215619f17a9fd6333d771cc29ce4a38937f060b1e45530f", + "sha256:7472797986d83016f96fde7f73577d129b0cd8a8d0b783487a7be330d57ba431" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0.20240106" + "version": "==5.1.0.20240311" }, "types-paho-mqtt": { "hashes": [ @@ -1391,12 +1804,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "urllib3": { "hashes": [ diff --git a/robot-server/robot_server/app_setup.py b/robot-server/robot_server/app_setup.py index 80fda9611196..04147753906c 100644 --- a/robot-server/robot_server/app_setup.py +++ b/robot-server/robot_server/app_setup.py @@ -36,7 +36,7 @@ ) from .service.notifications import ( - initialize_notification_client, + initialize_notifications, clean_up_notification_client, ) @@ -106,7 +106,7 @@ async def on_startup() -> None: fbl_mark_persistence_init_complete ], ) - initialize_notification_client( + await initialize_notifications( app_state=app.state, ) diff --git a/robot-server/robot_server/deck_configuration/defaults.py b/robot-server/robot_server/deck_configuration/defaults.py index a591e9798dff..3ed9a5ed3951 100644 --- a/robot-server/robot_server/deck_configuration/defaults.py +++ b/robot-server/robot_server/deck_configuration/defaults.py @@ -7,40 +7,64 @@ _for_flex = models.DeckConfigurationRequest.construct( cutoutFixtures=[ models.CutoutFixture.construct( - cutoutId="cutoutA1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutA1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutB1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutC1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD1", cutoutFixtureId="singleLeftSlot" + cutoutId="cutoutD1", + cutoutFixtureId="singleLeftSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutA2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutA2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutB2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutC2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD2", cutoutFixtureId="singleCenterSlot" + cutoutId="cutoutD2", + cutoutFixtureId="singleCenterSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutA3", cutoutFixtureId="trashBinAdapter" + cutoutId="cutoutA3", + cutoutFixtureId="trashBinAdapter", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutB3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutB3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutC3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutC3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutoutD3", cutoutFixtureId="singleRightSlot" + cutoutId="cutoutD3", + cutoutFixtureId="singleRightSlot", + opentronsModuleSerialNumber=None, ), ] ) @@ -49,40 +73,64 @@ _for_ot2 = models.DeckConfigurationRequest.construct( cutoutFixtures=[ models.CutoutFixture.construct( - cutoutId="cutout1", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout1", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout2", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout2", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout3", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout3", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout4", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout4", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout5", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout5", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout6", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout6", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout7", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout7", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout8", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout8", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout9", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout9", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout10", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout10", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout11", cutoutFixtureId="singleStandardSlot" + cutoutId="cutout11", + cutoutFixtureId="singleStandardSlot", + opentronsModuleSerialNumber=None, ), models.CutoutFixture.construct( - cutoutId="cutout12", cutoutFixtureId="fixedTrashSlot" + cutoutId="cutout12", + cutoutFixtureId="fixedTrashSlot", + opentronsModuleSerialNumber=None, ), ] ) diff --git a/robot-server/robot_server/deck_configuration/models.py b/robot-server/robot_server/deck_configuration/models.py index f0d2a7cd6bdf..b84b395a6679 100644 --- a/robot-server/robot_server/deck_configuration/models.py +++ b/robot-server/robot_server/deck_configuration/models.py @@ -33,6 +33,13 @@ class CutoutFixture(pydantic.BaseModel): " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." ) ) + opentronsModuleSerialNumber: Optional[str] = pydantic.Field( + description=( + "The serial number of a module loaded as a fixture." + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + default=None, + ) class DeckConfigurationRequest(pydantic.BaseModel): diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py index 4e00a3d707e0..6e9d68d9f1b2 100644 --- a/robot-server/robot_server/deck_configuration/router.py +++ b/robot-server/robot_server/deck_configuration/router.py @@ -7,7 +7,7 @@ import fastapi from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV5 from robot_server.errors.error_responses import ErrorBody from robot_server.hardware import get_deck_definition @@ -64,7 +64,7 @@ async def put_deck_configuration( # noqa: D103 request_body: RequestModel[models.DeckConfigurationRequest], store: DeckConfigurationStore = fastapi.Depends(get_deck_configuration_store), now: datetime = fastapi.Depends(get_current_time), - deck_definition: DeckDefinitionV4 = fastapi.Depends(get_deck_definition), + deck_definition: DeckDefinitionV5 = fastapi.Depends(get_deck_definition), ) -> PydanticResponse[ Union[ SimpleBody[models.DeckConfigurationResponse], diff --git a/robot-server/robot_server/deck_configuration/store.py b/robot-server/robot_server/deck_configuration/store.py index feffa539ec05..e892c91f7e5c 100644 --- a/robot-server/robot_server/deck_configuration/store.py +++ b/robot-server/robot_server/deck_configuration/store.py @@ -54,7 +54,9 @@ async def set( path=self._path, cutout_fixture_placements=[ calibration_storage_types.CutoutFixturePlacement( - cutout_fixture_id=e.cutoutFixtureId, cutout_id=e.cutoutId + cutout_fixture_id=e.cutoutFixtureId, + cutout_id=e.cutoutId, + opentrons_module_serial_number=e.opentronsModuleSerialNumber, ) for e in request.cutoutFixtures ], @@ -71,7 +73,8 @@ async def get_deck_configuration(self) -> DeckConfigurationType: """Get the robot's current deck configuration in an expected typing.""" to_convert = await self.get() converted = [ - (item.cutoutId, item.cutoutFixtureId) for item in to_convert.cutoutFixtures + (item.cutoutId, item.cutoutFixtureId, item.opentronsModuleSerialNumber) + for item in to_convert.cutoutFixtures ] return converted @@ -102,6 +105,7 @@ async def _get_assuming_locked(self) -> models.DeckConfigurationResponse: models.CutoutFixture.construct( cutoutFixtureId=e.cutout_fixture_id, cutoutId=e.cutout_id, + opentronsModuleSerialNumber=e.opentrons_module_serial_number, ) for e in cutout_fixtures_from_storage ] diff --git a/robot-server/robot_server/deck_configuration/validation.py b/robot-server/robot_server/deck_configuration/validation.py index 0530a4f9271c..a3c043f8f511 100644 --- a/robot-server/robot_server/deck_configuration/validation.py +++ b/robot-server/robot_server/deck_configuration/validation.py @@ -3,7 +3,7 @@ from collections import defaultdict from dataclasses import dataclass -from typing import DefaultDict, FrozenSet, List, Set, Tuple, Union +from typing import DefaultDict, FrozenSet, List, Set, Tuple, Union, Optional from opentrons_shared_data.deck import dev_types as deck_types @@ -14,6 +14,7 @@ class Placement: cutout_id: str cutout_fixture_id: str + opentrons_module_serial_number: Optional[str] @dataclass(frozen=True) @@ -43,22 +44,51 @@ class InvalidLocationError: @dataclass(frozen=True) class UnrecognizedCutoutFixtureError: - """When an cutout fixture has been mounted that's not defined by the deck definition.""" + """When a cutout fixture has been mounted that's not defined by the deck definition.""" cutout_fixture_id: str allowed_cutout_fixture_ids: FrozenSet[str] +@dataclass(frozen=True) +class InvalidSerialNumberError: + """When a module cutout fixture has been mounted but not given a serial number.""" + + cutout_id: str + cutout_fixture_id: str + + +@dataclass(frozen=True) +class UnexpectedSerialNumberError: + """When a cutout fixture that is not a module has been provided a serial number.""" + + cutout_id: str + cutout_fixture_id: str + opentrons_module_serial_number: str + + +@dataclass(frozen=True) +class MissingGroupFixtureError: + """When a member of a fixture group has been mounted but other required members of that group have not.""" + + cutout_id: str + cutout_fixture_id: str + missing_fixture_id: str + + ConfigurationError = Union[ UnoccupiedCutoutError, OvercrowdedCutoutError, InvalidLocationError, UnrecognizedCutoutFixtureError, + InvalidSerialNumberError, + UnexpectedSerialNumberError, + MissingGroupFixtureError, ] -def get_configuration_errors( - deck_definition: deck_types.DeckDefinitionV4, +def get_configuration_errors( # noqa: C901 + deck_definition: deck_types.DeckDefinitionV5, placements: List[Placement], ) -> Set[ConfigurationError]: """Return all the problems with the given deck configration. @@ -98,12 +128,55 @@ def get_configuration_errors( allowed_cutout_ids=allowed_cutout_ids, ) ) + if found_cutout_fixture[ + "expectOpentronsModuleSerialNumber" + ] is False and isinstance(placement.opentrons_module_serial_number, str): + errors.add( + UnexpectedSerialNumberError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + opentrons_module_serial_number=placement.opentrons_module_serial_number, + ) + ) + elif ( + found_cutout_fixture["expectOpentronsModuleSerialNumber"] is True + and placement.opentrons_module_serial_number is None + ): + errors.add( + InvalidSerialNumberError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + ) + ) + for cutout_id in found_cutout_fixture["fixtureGroup"]: + if cutout_id == placement.cutout_id: + map = found_cutout_fixture["fixtureGroup"][cutout_id] + member_found = False + for item in map: + for group_member_cutout_id in item: + group_member_fixture_id = item[group_member_cutout_id] + for deck_item in placements: + if ( + group_member_fixture_id + == deck_item.cutout_fixture_id + and group_member_cutout_id == deck_item.cutout_id + ): + member_found = True + if member_found is False: + errors.add( + MissingGroupFixtureError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + missing_fixture_id=group_member_fixture_id, + ) + ) + member_found = False return errors def _find_cutout_fixture( - deck_definition: deck_types.DeckDefinitionV4, cutout_fixture_id: str + deck_definition: deck_types.DeckDefinitionV5, cutout_fixture_id: str ) -> Union[deck_types.CutoutFixture, UnrecognizedCutoutFixtureError]: cutout_fixtures = deck_definition["cutoutFixtures"] try: diff --git a/robot-server/robot_server/deck_configuration/validation_mapping.py b/robot-server/robot_server/deck_configuration/validation_mapping.py index 10d9b65158a6..1337218075c6 100644 --- a/robot-server/robot_server/deck_configuration/validation_mapping.py +++ b/robot-server/robot_server/deck_configuration/validation_mapping.py @@ -10,7 +10,11 @@ def map_in(request: models.DeckConfigurationRequest) -> List[validation.Placement]: """Map a request from HTTP to internal types that can be validated.""" return [ - validation.Placement(cutout_id=p.cutoutId, cutout_fixture_id=p.cutoutFixtureId) + validation.Placement( + cutout_id=p.cutoutId, + cutout_fixture_id=p.cutoutFixtureId, + opentrons_module_serial_number=p.opentronsModuleSerialNumber, + ) for p in request.cutoutFixtures ] diff --git a/robot-server/robot_server/hardware.py b/robot-server/robot_server/hardware.py index c72a162b1bec..2994248a302d 100644 --- a/robot-server/robot_server/hardware.py +++ b/robot-server/robot_server/hardware.py @@ -381,9 +381,9 @@ async def get_deck_type() -> DeckType: async def get_deck_definition( deck_type: DeckType = Depends(get_deck_type), -) -> deck.dev_types.DeckDefinitionV4: +) -> deck.dev_types.DeckDefinitionV5: """Return this robot's deck definition.""" - return deck.load(deck_type, version=4) + return deck.load(deck_type, version=5) async def _postinit_ot2_tasks( diff --git a/robot-server/robot_server/maintenance_runs/maintenance_action_models.py b/robot-server/robot_server/maintenance_runs/maintenance_action_models.py deleted file mode 100644 index 1eb34809dd57..000000000000 --- a/robot-server/robot_server/maintenance_runs/maintenance_action_models.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Request and response models for controlling maintenance runs with actions.""" -from datetime import datetime -from enum import Enum -from pydantic import BaseModel, Field - -from robot_server.service.json_api import ResourceModel - - -class MaintenanceRunActionType(str, Enum): - """Types of run control actions. - - Args: - PLAY: Start or resume a protocol run. - PAUSE: Pause a run. - STOP: Stop (cancel) a run. - """ - - PLAY = "play" - PAUSE = "pause" - STOP = "stop" - - -class MaintenanceRunActionCreate(BaseModel): - """Request model for new control action creation.""" - - actionType: MaintenanceRunActionType - - -class MaintenanceRunAction(ResourceModel): - """Maintenance Run control action model. - - A MaintenanceRunAction resource represents a client-provided command to - the run in order to control the execution of the run itself. - - This is different than a run command, which represents an individual - robotic procedure to be executed. - """ - - id: str = Field(..., description="A unique identifier to reference the command.") - createdAt: datetime = Field(..., description="When the command was created.") - actionType: MaintenanceRunActionType = Field( - ..., - description="Specific type of action, which determines behavior.", - ) diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 8e42cbf2caec..c70d2a1dd070 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -1,7 +1,10 @@ """In-memory storage of ProtocolEngine instances.""" +import asyncio +import logging from datetime import datetime -from typing import List, NamedTuple, Optional +from typing import List, NamedTuple, Optional, Callable +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.robot.dev_types import RobotTypeEnum @@ -27,6 +30,9 @@ from opentrons.protocol_engine.types import DeckConfigurationType +_log = logging.getLogger(__name__) + + class EngineConflictError(RuntimeError): """An error raised if an active engine is already initialized. @@ -48,18 +54,47 @@ class RunnerEnginePair(NamedTuple): engine: ProtocolEngine -def get_estop_listener(engine_store: "MaintenanceEngineStore") -> HardwareEventHandler: - """Create a callback for estop events.""" +async def handle_estop_event( + engine_store: "MaintenanceEngineStore", event: HardwareEvent +) -> None: + """Handle an E-stop event from the hardware API. - def _callback(event: HardwareEvent) -> None: + This is meant to run in the engine's thread and asyncio event loop. + + This is a public function for unit-testing purposes, but it's an implementation + detail of the store. + """ + try: if isinstance(event, EstopStateNotification): if event.new_state is not EstopState.PHYSICALLY_ENGAGED: return if engine_store.current_run_id is None: return - engine_store.engine.estop(maintenance_run=True) + # todo(mm, 2024-04-17): This estop teardown sequencing belongs in the + # runner layer. + engine_store.engine.estop() + await engine_store.engine.finish(error=EStopActivatedError()) + except Exception: + # This is a background task kicked off by a hardware event, + # so there's no one to propagate this exception to. + _log.exception("Exception handling E-stop event.") + + +def _get_estop_listener(engine_store: "MaintenanceEngineStore") -> HardwareEventHandler: + """Create a callback for estop events. + + The returned callback is meant to run in the hardware API's thread. + """ + engine_loop = asyncio.get_running_loop() - return _callback + def run_handler_in_engine_thread_from_hardware_thread( + event: HardwareEvent, + ) -> None: + asyncio.run_coroutine_threadsafe( + handle_estop_event(engine_store, event), engine_loop + ) + + return run_handler_in_engine_thread_from_hardware_thread class MaintenanceEngineStore: @@ -83,15 +118,7 @@ def __init__( self._robot_type = robot_type self._deck_type = deck_type self._runner_engine_pair: Optional[RunnerEnginePair] = None - hardware_api.register_callback(get_estop_listener(self)) - - def _estop_listener(self, event: HardwareEvent) -> None: - if isinstance(event, EstopStateNotification): - if event.new_state is not EstopState.PHYSICALLY_ENGAGED: - return - if self._runner_engine_pair is None: - return - self._runner_engine_pair.engine.estop(maintenance_run=True) + hardware_api.register_callback(_get_estop_listener(self)) @property def engine(self) -> ProtocolEngine: @@ -127,6 +154,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + notify_publishers: Callable[[], None], deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -135,6 +163,7 @@ async def create( run_id: The run resource the engine is assigned to. created_at: Run creation datetime labware_offsets: Labware offsets to create the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The initial equipment and status summary of the engine. @@ -154,6 +183,7 @@ async def create( ), ), deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) # Using LiveRunner as the runner to allow for future refactor of maintenance runs diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index 9857c50a2004..084a7552a3a8 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -1,6 +1,6 @@ """Manage current maintenance run data.""" from datetime import datetime -from typing import List, Optional +from typing import List, Optional, Callable from opentrons.protocol_engine import ( EngineStatus, @@ -83,6 +83,7 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], ) -> MaintenanceRun: """Create a new, current maintenance run. @@ -90,6 +91,7 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + notify_publishers: Utilized by the engine to notify publishers of state changes. Returns: The run resource. @@ -102,6 +104,7 @@ async def create( created_at=created_at, labware_offsets=labware_offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) maintenance_run_data = _build_run( diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py index f4d1a19dc61f..00379034d9bc 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_models.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_models.py @@ -17,7 +17,6 @@ LabwareOffsetCreate, Liquid, ) -from robot_server.maintenance_runs.maintenance_action_models import MaintenanceRunAction from robot_server.service.json_api import ResourceModel @@ -70,9 +69,15 @@ class MaintenanceRun(ResourceModel): " There can be, at most, one current run." ), ) - actions: List[MaintenanceRunAction] = Field( + actions: List[object] = Field( ..., - description="Client-initiated run control actions.", + description=( + " This is currently always an empty list," + " and is provided for symmetry with non-maintenance runs." + " Non-maintenance runs let you issue actions with" + " `POST /runs/{id}/actions`, but there is currently no equivalent" + " endpoint for maintenance runs." + ), ) errors: List[ErrorOccurrence] = Field( ..., diff --git a/robot-server/robot_server/maintenance_runs/router/base_router.py b/robot-server/robot_server/maintenance_runs/router/base_router.py index d2eb71a57986..c115d46509f5 100644 --- a/robot-server/robot_server/maintenance_runs/router/base_router.py +++ b/robot-server/robot_server/maintenance_runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional +from typing import Optional, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status @@ -39,6 +39,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -155,6 +156,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[MaintenanceRun]]: """Create a new maintenance run. @@ -166,6 +168,7 @@ async def create_run( is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ if not is_ok_to_create_maintenance_run: raise ProtocolRunIsActive( @@ -180,6 +183,7 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) log.info(f'Created an empty run "{run_id}"".') diff --git a/robot-server/robot_server/persistence/_migrations/v3_to_v4.py b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py new file mode 100644 index 000000000000..b67d11d34ecc --- /dev/null +++ b/robot-server/robot_server/persistence/_migrations/v3_to_v4.py @@ -0,0 +1,58 @@ +"""Migrate the persistence directory from schema 3 to 4. + +Summary of changes from schema 3: + +- Adds a new "run_time_parameter_values_and_defaults" column to analysis table +- Adds a new "run_time_parameters" column to run table +""" + +from pathlib import Path +from contextlib import ExitStack +import shutil +from typing import Any + +import sqlalchemy + +from ..database import sql_engine_ctx +from ..tables import schema_4 +from .._folder_migrator import Migration + +_DB_FILE = "robot_server.db" + + +class Migration3to4(Migration): # noqa: D101 + def migrate(self, source_dir: Path, dest_dir: Path) -> None: + """Migrate the persistence directory from schema 3 to 4.""" + # Copy over all existing directories and files to new version + for item in source_dir.iterdir(): + if item.is_dir(): + shutil.copytree(src=item, dst=dest_dir / item.name) + else: + shutil.copy(src=item, dst=dest_dir / item.name) + dest_db_file = dest_dir / _DB_FILE + + # Append the new column to existing analyses in v4 database + with ExitStack() as exit_stack: + dest_engine = exit_stack.enter_context(sql_engine_ctx(dest_db_file)) + schema_4.metadata.create_all(dest_engine) + + def add_column( + engine: sqlalchemy.engine.Engine, + table_name: str, + column: Any, + ) -> None: + column_type = column.type.compile(engine.dialect) + engine.execute( + f"ALTER TABLE {table_name} ADD COLUMN {column.key} {column_type}" + ) + + add_column( + dest_engine, + schema_4.analysis_table.name, + schema_4.analysis_table.c.run_time_parameter_values_and_defaults, + ) + add_column( + dest_engine, + schema_4.run_table.name, + schema_4.run_table.c.run_time_parameters, + ) diff --git a/robot-server/robot_server/persistence/persistence_directory.py b/robot-server/robot_server/persistence/persistence_directory.py index 666d5c7998fb..b7982b38555b 100644 --- a/robot-server/robot_server/persistence/persistence_directory.py +++ b/robot-server/robot_server/persistence/persistence_directory.py @@ -11,7 +11,7 @@ from anyio import Path as AsyncPath, to_thread from ._folder_migrator import MigrationOrchestrator -from ._migrations import up_to_3 +from ._migrations import up_to_3, v3_to_v4 _TEMP_PERSISTENCE_DIR_PREFIX: Final = "opentrons-robot-server-" @@ -48,7 +48,10 @@ async def prepare_active_subdirectory(prepared_root: Path) -> Path: """Return the active persistence subdirectory after preparing it, if necessary.""" migration_orchestrator = MigrationOrchestrator( root=prepared_root, - migrations=[up_to_3.MigrationUpTo3(subdirectory="3")], + migrations=[ + up_to_3.MigrationUpTo3(subdirectory="3"), + v3_to_v4.Migration3to4(subdirectory="4"), + ], temp_file_prefix="temp-", ) diff --git a/robot-server/robot_server/persistence/pydantic.py b/robot-server/robot_server/persistence/pydantic.py index c3486394ad47..c56312ec166f 100644 --- a/robot-server/robot_server/persistence/pydantic.py +++ b/robot-server/robot_server/persistence/pydantic.py @@ -1,7 +1,8 @@ """Store Pydantic objects in the SQL database.""" -from typing import Type, TypeVar -from pydantic import BaseModel, parse_raw_as +import json +from typing import Type, TypeVar, List, Sequence +from pydantic import BaseModel, parse_raw_as, parse_obj_as _BaseModelT = TypeVar("_BaseModelT", bound=BaseModel) @@ -17,6 +18,16 @@ def pydantic_to_json(obj: BaseModel) -> str: ) -def json_to_pydantic(model: Type[_BaseModelT], json: str) -> _BaseModelT: +def pydantic_list_to_json(obj_list: Sequence[BaseModel]) -> str: + """Serialize a list of Pydantic objects for storing in the SQL database.""" + return json.dumps([obj.dict(by_alias=True, exclude_none=True) for obj in obj_list]) + + +def json_to_pydantic(model: Type[_BaseModelT], json_str: str) -> _BaseModelT: """Parse a Pydantic object stored in the SQL database.""" - return parse_raw_as(model, json) + return parse_raw_as(model, json_str) + + +def json_to_pydantic_list(model: Type[_BaseModelT], json_str: str) -> List[_BaseModelT]: + """Parse a list of Pydantic objects stored in the SQL database.""" + return [parse_obj_as(model, obj_dict) for obj_dict in json.loads(json_str)] diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 97262e73fab8..0aaf869fb354 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -1,7 +1,7 @@ """SQL database schemas.""" # Re-export the latest schema. -from .schema_3 import ( +from .schema_4 import ( metadata, protocol_table, analysis_table, diff --git a/robot-server/robot_server/persistence/tables/schema_4.py b/robot-server/robot_server/persistence/tables/schema_4.py new file mode 100644 index 000000000000..d1662bf7adc2 --- /dev/null +++ b/robot-server/robot_server/persistence/tables/schema_4.py @@ -0,0 +1,137 @@ +"""v4 of our SQLite schema.""" + +import sqlalchemy + +from robot_server.persistence._utc_datetime import UTCDateTime + +metadata = sqlalchemy.MetaData() + +protocol_table = sqlalchemy.Table( + "protocol", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column("protocol_key", sqlalchemy.String, nullable=True), +) + +analysis_table = sqlalchemy.Table( + "analysis", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + index=True, + nullable=False, + ), + sqlalchemy.Column( + "analyzer_version", + sqlalchemy.String, + nullable=False, + ), + sqlalchemy.Column( + "completed_analysis", + # Stores a JSON string. See CompletedAnalysisStore. + sqlalchemy.String, + nullable=False, + ), + # column added in schema v4 + sqlalchemy.Column( + "run_time_parameter_values_and_defaults", + sqlalchemy.String, + nullable=True, + ), +) + +run_table = sqlalchemy.Table( + "run", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column( + "created_at", + UTCDateTime, + nullable=False, + ), + sqlalchemy.Column( + "protocol_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("protocol.id"), + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column( + "state_summary", + sqlalchemy.String, + nullable=True, + ), + # column added in schema v1 + sqlalchemy.Column("engine_status", sqlalchemy.String, nullable=True), + # column added in schema v1 + sqlalchemy.Column("_updated_at", UTCDateTime, nullable=True), + # column added in schema v4 + sqlalchemy.Column( + "run_time_parameters", + # Stores a JSON string. See RunStore. + sqlalchemy.String, + nullable=True, + ), +) + +action_table = sqlalchemy.Table( + "action", + metadata, + sqlalchemy.Column( + "id", + sqlalchemy.String, + primary_key=True, + ), + sqlalchemy.Column("created_at", UTCDateTime, nullable=False), + sqlalchemy.Column("action_type", sqlalchemy.String, nullable=False), + sqlalchemy.Column( + "run_id", + sqlalchemy.String, + sqlalchemy.ForeignKey("run.id"), + nullable=False, + ), +) + +run_command_table = sqlalchemy.Table( + "run_command", + metadata, + sqlalchemy.Column("row_id", sqlalchemy.Integer, primary_key=True), + sqlalchemy.Column( + "run_id", sqlalchemy.String, sqlalchemy.ForeignKey("run.id"), nullable=False + ), + sqlalchemy.Column("index_in_run", sqlalchemy.Integer, nullable=False), + sqlalchemy.Column("command_id", sqlalchemy.String, nullable=False), + sqlalchemy.Column("command", sqlalchemy.String, nullable=False), + sqlalchemy.Index( + "ix_run_run_id_command_id", # An arbitrary name for the index. + "run_id", + "command_id", + unique=True, + ), + sqlalchemy.Index( + "ix_run_run_id_index_in_run", # An arbitrary name for the index. + "run_id", + "index_in_run", + unique=True, + ), +) diff --git a/robot-server/robot_server/protocols/analysis_memcache.py b/robot-server/robot_server/protocols/analysis_memcache.py index 19280009bd53..3ba3156607fc 100644 --- a/robot-server/robot_server/protocols/analysis_memcache.py +++ b/robot-server/robot_server/protocols/analysis_memcache.py @@ -63,3 +63,14 @@ def insert(self, key: K, value: V) -> None: self._pop_eldest(key) self._cache[key] = value self._cache_order.appendleft(key) + + def remove(self, key: K) -> None: + """Remove the cached element specified by the key. + + If no such element exists in cache, then simply no-op. + """ + try: + self._cache.pop(key) + self._cache_order.remove(key) # O(n) operation, use sparingly + except KeyError: + pass diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 0a3c64c9db0d..c8b11f2db251 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -2,10 +2,10 @@ # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum -from opentrons.protocol_engine.types import RunTimeParameter +from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field -from typing import List, Optional, Union +from typing import List, Optional, Union, NamedTuple from typing_extensions import Literal from opentrons.protocol_engine import ( @@ -40,6 +40,18 @@ class AnalysisResult(str, Enum): NOT_OK = "not-ok" +class AnalysisRequest(BaseModel): + """Model for analysis request body.""" + + runTimeParameterValues: RunTimeParamValuesType = Field( + default={}, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) + forceReAnalyze: bool = Field( + False, description="Whether to force start a new analysis." + ) + + class AnalysisSummary(BaseModel): """Base model for an analysis of a protocol.""" @@ -150,4 +162,11 @@ class CompletedAnalysis(BaseModel): ) +class RunTimeParameterAnalysisData(NamedTuple): + """Data from analysis of a run-time parameter.""" + + value: Union[float, bool, str] + default: Union[float, bool, str] + + ProtocolAnalysis = Union[PendingAnalysis, CompletedAnalysis] diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index d8ce780f98d2..4f5b66ed4f8e 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -19,6 +19,7 @@ LoadedModule, Liquid, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from .analysis_models import ( AnalysisSummary, @@ -27,6 +28,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from .completed_analysis_store import CompletedAnalysisStore, CompletedAnalysisResource @@ -71,6 +73,14 @@ def __init__(self, analysis_id: str) -> None: super().__init__(f'Analysis "{analysis_id}" not found.') +class AnalysisIsPendingError(RuntimeError): + """Exception raised if a given analysis is still pending.""" + + def __init__(self, analysis_id: str) -> None: + """Initialize the error's message.""" + super().__init__(f'Analysis "{analysis_id}" is still pending.') + + # TODO(sf, 2023-05-05): Like for protocols and runs, there's an in-memory cache for # elements of this store. Unlike for protocols and runs, it isn't just an lru_cache # on the top-level store's access methods, because those access methods have to be @@ -93,10 +103,14 @@ class AnalysisStore: so they're only kept in-memory, and lost when the store instance is destroyed. """ - def __init__(self, sql_engine: sqlalchemy.engine.Engine) -> None: + def __init__( + self, + sql_engine: sqlalchemy.engine.Engine, + completed_store: Optional[CompletedAnalysisStore] = None, + ) -> None: """Initialize the `AnalysisStore`.""" self._pending_store = _PendingAnalysisStore() - self._completed_store = CompletedAnalysisStore( + self._completed_store = completed_store or CompletedAnalysisStore( sql_engine=sql_engine, memory_cache=MemoryCache(_CACHE_MAX_SIZE, str, CompletedAnalysisResource), current_analyzer_version=_CURRENT_ANALYZER_VERSION, @@ -115,8 +129,6 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: Returns: A summary of the just-added analysis. """ - # TODO (spp, 2024-03-19): cap the number of analyses being stored by - # auto-deleting old ones new_pending_analysis = self._pending_store.add( protocol_id=protocol_id, analysis_id=analysis_id ) @@ -180,8 +192,11 @@ async def update( protocol_id=protocol_id, analyzer_version=_CURRENT_ANALYZER_VERSION, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=self._extract_run_time_param_values_and_defaults( + completed_analysis + ), ) - await self._completed_store.add( + await self._completed_store.make_room_and_add( completed_analysis_resource=completed_analysis_resource ) @@ -258,6 +273,88 @@ async def get_by_protocol(self, protocol_id: str) -> List[ProtocolAnalysis]: else: return completed_analyses + [pending_analysis] + @staticmethod + def _extract_run_time_param_values_and_defaults( + completed_analysis: CompletedAnalysis, + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Extract the Run Time Parameters with current value and default value of each. + + We do this in order to save the RTP data separately, outside the analysis + in the database. This saves us from having to de-serialize the entire analysis + to read just the RTP values. + """ + rtp_list = completed_analysis.runTimeParameters + + rtp_values_and_defaults = {} + for param_spec in rtp_list: + rtp_values_and_defaults.update( + { + param_spec.variableName: RunTimeParameterAnalysisData( + value=param_spec.value, default=param_spec.default + ) + } + ) + return rtp_values_and_defaults + + async def matching_rtp_values_in_analysis( + self, analysis_summary: AnalysisSummary, new_rtp_values: RunTimeParamValuesType + ) -> bool: + """Return whether the last analysis of the given protocol used the mentioned RTP values. + + It is not sufficient to just check the values of provided parameters against the + corresponding parameter values in analysis because a previous request could have + composed of some extra parameters that are not in the current list. + + Similarly, it is not enough to only compare the current parameter values from + the client with the previous values from the client because a previous param + might have been assigned a default value by the client while the current request + doesn't include that param because it can rely on the API to assign the default + value to that param. + + So, we check that the Run Time Parameters in the previous analysis has params + with the values provided in the current request, and also verify that rest of the + parameters in the analysis use default values. + """ + if analysis_summary.status == AnalysisStatus.PENDING: + raise AnalysisIsPendingError(analysis_summary.id) + + rtp_values_and_defaults_in_last_analysis = ( + await self._completed_store.get_rtp_values_and_defaults_by_analysis_id( + analysis_summary.id + ) + ) + # We already make sure that the protocol has an analysis associated with before + # checking the RTP values so this assert should never raise. + # It is only added for type checking. + assert ( + rtp_values_and_defaults_in_last_analysis is not None + ), "This protocol has no analysis associated with it." + + if not set(new_rtp_values.keys()).issubset( + set(rtp_values_and_defaults_in_last_analysis.keys()) + ): + # Since the RTP keys in analysis represent all params defined in the protocol, + # if the client passes a parameter that's not present in the analysis, + # it means that the client is sending incorrect parameters. + # We will let this request trigger an analysis using the incorrect params + # and have the analysis raise an appropriate error instead of giving an + # error response to the protocols request. + # This makes the behavior of robot server consistent regardless of whether + # the client is sending a protocol for the first time or for the nth time. + return False + for ( + parameter, + prev_value_and_default, + ) in rtp_values_and_defaults_in_last_analysis.items(): + if ( + new_rtp_values.get(parameter, prev_value_and_default.default) + == prev_value_and_default.value + ): + continue + else: + return False + return True + class _PendingAnalysisStore: """An in-memory store of protocol analyses that are pending. diff --git a/robot-server/robot_server/protocols/completed_analysis_store.py b/robot-server/robot_server/protocols/completed_analysis_store.py index f4c696d05195..5f72357050be 100644 --- a/robot-server/robot_server/protocols/completed_analysis_store.py +++ b/robot-server/robot_server/protocols/completed_analysis_store.py @@ -2,23 +2,27 @@ from __future__ import annotations import asyncio +import json from typing import Dict, List, Optional from logging import getLogger from dataclasses import dataclass import sqlalchemy import anyio +from pydantic import parse_raw_as from robot_server.persistence.database import sqlite_rowid from robot_server.persistence.tables import analysis_table from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json -from .analysis_models import CompletedAnalysis +from .analysis_models import CompletedAnalysis, RunTimeParameterAnalysisData from .analysis_memcache import MemoryCache _log = getLogger(__name__) +MAX_ANALYSES_TO_STORE = 5 + @dataclass class CompletedAnalysisResource: @@ -31,6 +35,7 @@ class CompletedAnalysisResource: protocol_id: str analyzer_version: str completed_analysis: CompletedAnalysis + run_time_parameter_values_and_defaults: Dict[str, RunTimeParameterAnalysisData] async def to_sql_values(self) -> Dict[str, object]: """Return this data as a dict that can be passed to a SQLALchemy insert. @@ -46,18 +51,25 @@ async def to_sql_values(self) -> Dict[str, object]: def serialize_completed_analysis() -> str: return pydantic_to_json(self.completed_analysis) - serialized_json = await anyio.to_thread.run_sync( + def serialize_rtp_dict() -> str: + return json.dumps(self.run_time_parameter_values_and_defaults) + + serialized_analysis = await anyio.to_thread.run_sync( serialize_completed_analysis, # Cancellation may orphan the worker thread, # but that should be harmless in this case. cancellable=True, ) - + serialized_rtp_dict = await anyio.to_thread.run_sync( + serialize_rtp_dict, + cancellable=True, + ) return { "id": self.id, "protocol_id": self.protocol_id, "analyzer_version": self.analyzer_version, - "completed_analysis": serialized_json, + "completed_analysis": serialized_analysis, + "run_time_parameter_values_and_defaults": serialized_rtp_dict, } @classmethod @@ -94,12 +106,40 @@ def parse_completed_analysis() -> CompletedAnalysis: # but that should be harmless in this case. cancellable=True, ) - + rtp_values_and_defaults = await cls.get_run_time_parameter_values_and_defaults( + sql_row + ) return cls( id=id, protocol_id=protocol_id, analyzer_version=analyzer_version, completed_analysis=completed_analysis, + run_time_parameter_values_and_defaults=rtp_values_and_defaults, + ) + + @classmethod + async def get_run_time_parameter_values_and_defaults( + cls, sql_row: sqlalchemy.engine.Row + ) -> Dict[str, RunTimeParameterAnalysisData]: + """Get the run-time parameters used in the analysis with their values & defaults.""" + + def parse_rtp_dict() -> Dict[str, RunTimeParameterAnalysisData]: + rtp_contents = sql_row.run_time_parameter_values_and_defaults + return ( + parse_raw_as( + Dict[str, RunTimeParameterAnalysisData], + sql_row.run_time_parameter_values_and_defaults, + ) + if rtp_contents + else {} + ) + + # In most cases, this parsing should be quite quick but theoretically + # there could be an unexpectedly large number of run time params. + # So we delegate the parsing of this to a cancellable thread as well. + return await anyio.to_thread.run_sync( + parse_rtp_dict, + cancellable=True, ) @@ -185,6 +225,40 @@ async def get_by_id_as_document(self, analysis_id: str) -> Optional[str]: return document + async def get_rtp_values_and_defaults_by_analysis_id( + self, analysis_id: str + ) -> Optional[Dict[str, RunTimeParameterAnalysisData]]: + """Return the dictionary of run time parameter values & defaults used in the given analysis. + + If the analysis ID doesn't exist, return None. + These RTP values are not cached in memory by themselves since we don't anticipate + that fetching the values from the database to be a time-consuming operation. + """ + async with self._memcache_lock: + try: + analysis = self._memcache.get(analysis_id) + except KeyError: + pass + else: + return analysis.run_time_parameter_values_and_defaults + + statement = sqlalchemy.select(analysis_table).where( + analysis_table.c.id == analysis_id + ) + with self._sql_engine.begin() as transaction: + try: + result = transaction.execute(statement).one() + except sqlalchemy.exc.NoResultFound: + # Since we just no-op when fetching non-existent analysis, + # do the same for non-existent RTP data + return None + + rtp_values_and_defaults = await CompletedAnalysisResource.get_run_time_parameter_values_and_defaults( + result + ) + + return rtp_values_and_defaults + async def get_by_protocol( self, protocol_id: str ) -> List[CompletedAnalysisResource]: @@ -262,13 +336,34 @@ def get_ids_by_protocol(self, protocol_id: str) -> List[str]: return result_ids - async def add(self, completed_analysis_resource: CompletedAnalysisResource) -> None: - """Add a resource to the store.""" - statement = analysis_table.insert().values( + async def make_room_and_add( + self, completed_analysis_resource: CompletedAnalysisResource + ) -> None: + """Make room and add a resource to the store. + + Removes the oldest analyses in store if the number of analyses exceed + the max allowed, and then adds the new analysis. + """ + analyses_ids = self.get_ids_by_protocol(completed_analysis_resource.protocol_id) + + # Delete all analyses exceeding max number allowed, + # plus an additional one to create room for the new one. + # Most existing databases will not have multiple extra analyses per protocol + # but there would be some internally that added multiple analyses before + # we started capping the number of analyses. + analyses_to_delete = analyses_ids[: -MAX_ANALYSES_TO_STORE + 1] + for analysis_id in analyses_to_delete: + self._memcache.remove(analysis_id) + delete_statement = analysis_table.delete().where( + analysis_table.c.id.in_(analyses_to_delete) + ) + + insert_statement = analysis_table.insert().values( await completed_analysis_resource.to_sql_values() ) with self._sql_engine.begin() as transaction: - transaction.execute(statement) + transaction.execute(delete_statement) + transaction.execute(insert_statement) self._memcache.insert( completed_analysis_resource.id, completed_analysis_resource ) diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index fb72c938def0..d3375f535d4a 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -4,8 +4,9 @@ from textwrap import dedent from datetime import datetime from pathlib import Path -from typing import List, Optional, Union +from typing import List, Optional, Union, Tuple +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons_shared_data.robot import user_facing_robot_type from typing_extensions import Literal @@ -32,13 +33,14 @@ SimpleEmptyBody, MultiBodyMeta, PydanticResponse, + RequestModel, ) from .protocol_auto_deleter import ProtocolAutoDeleter from .protocol_models import Protocol, ProtocolFile, Metadata from .protocol_analyzer import ProtocolAnalyzer -from .analysis_store import AnalysisStore, AnalysisNotFoundError -from .analysis_models import ProtocolAnalysis +from .analysis_store import AnalysisStore, AnalysisNotFoundError, AnalysisIsPendingError +from .analysis_models import ProtocolAnalysis, AnalysisRequest, AnalysisSummary from .protocol_store import ( ProtocolStore, ProtocolResource, @@ -74,6 +76,13 @@ class AnalysisNotFound(ErrorDetails): title: str = "Protocol Analysis Not Found" +class LastAnalysisPending(ErrorDetails): + """An error returned when the most recent analysis of a protocol is still pending.""" + + id: Literal["LastAnalysisPending"] = "LastAnalysisPending" + title: str = "Last Analysis Still Pending." + + class ProtocolFilesInvalid(ErrorDetails): """An error returned when an uploaded protocol files are invalid.""" @@ -140,7 +149,9 @@ class ProtocolLinks(BaseModel): resource will be returned instead of creating duplicate ones. When a new protocol resource is created, an analysis is started for it. - See the `/protocols/{id}/analyses/` endpoints. + A new analysis is also started if the same protocol file is uploaded but with + a different set of run-time parameter values than the most recent request. + See the `/protocols/{id}/analyses/` endpoints for more details. """ ), status_code=status.HTTP_201_CREATED, @@ -150,6 +161,7 @@ class ProtocolLinks(BaseModel): status.HTTP_422_UNPROCESSABLE_ENTITY: { "model": ErrorBody[Union[ProtocolFilesInvalid, ProtocolRobotTypeMismatch]] }, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, }, ) async def create_protocol( @@ -214,7 +226,6 @@ async def create_protocol( # TODO(mm, 2024-02-07): Investigate whether the filename can actually be None. assert file.filename is not None buffered_files = await file_reader_writer.read(files=files) # type: ignore[arg-type] - if isinstance(run_time_parameter_values, str): # We have to do this isinstance check because if `runTimeParameterValues` is # not specified in the request, then it gets assigned a Form(None) value @@ -223,36 +234,36 @@ async def create_protocol( # so we can validate the data contents and return a better error response. parsed_rtp = json.loads(run_time_parameter_values) else: - parsed_rtp = None + parsed_rtp = {} content_hash = await file_hasher.hash(buffered_files) cached_protocol_id = protocol_store.get_id_by_hash(content_hash) if cached_protocol_id is not None: - # Protocol exists in database resource = protocol_store.get(protocol_id=cached_protocol_id) - if parsed_rtp: - # This protocol exists in database but needs to be re-analyzed with the - # passed-in RTP overrides - task_runner.run( - protocol_analyzer.analyze, - protocol_resource=resource, - analysis_id=analysis_id, - run_time_param_values=parsed_rtp, - ) - analysis_store.add_pending( + + try: + analysis_summaries, _ = await _start_new_analysis_if_necessary( protocol_id=cached_protocol_id, analysis_id=analysis_id, + rtp_values=parsed_rtp, + force_reanalyze=False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, ) - analyses = analysis_store.get_summaries_by_protocol( - protocol_id=cached_protocol_id - ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + data = Protocol.construct( id=cached_protocol_id, createdAt=resource.created_at, protocolType=resource.source.config.protocol_type, robotType=resource.source.robot_type, metadata=Metadata.parse_obj(resource.source.metadata), - analysisSummaries=analyses, + analysisSummaries=analysis_summaries, key=resource.protocol_key, files=[ ProtocolFile(name=f.path.name, role=f.role) @@ -331,6 +342,53 @@ async def create_protocol( ) +async def _start_new_analysis_if_necessary( + protocol_id: str, + analysis_id: str, + force_reanalyze: bool, + rtp_values: RunTimeParamValuesType, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> Tuple[List[AnalysisSummary], bool]: + """Check RTP values and start a new analysis if necessary. + + Returns a tuple of the latest list of analysis summaries (including any newly + started analysis) and whether a new analysis was started. + """ + resource = protocol_store.get(protocol_id=protocol_id) + analyses = analysis_store.get_summaries_by_protocol(protocol_id=protocol_id) + started_new_analysis = False + if ( + force_reanalyze + or + # Unexpected situations, like powering off the robot after a protocol upload + # but before the analysis is complete, can leave the protocol resource + # without an associated analysis. + len(analyses) == 0 + or + # The most recent analysis was done using different RTP values + not await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analyses[-1], new_rtp_values=rtp_values + ) + ): + task_runner.run( + protocol_analyzer.analyze, + protocol_resource=resource, + analysis_id=analysis_id, + run_time_param_values=rtp_values, + ) + started_new_analysis = True + analyses.append( + analysis_store.add_pending( + protocol_id=protocol_id, + analysis_id=analysis_id, + ) + ) + return analyses, started_new_analysis + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols", @@ -493,6 +551,78 @@ async def delete_protocol_by_id( ) +@PydanticResponse.wrap_route( + protocols_router.post, + path="/protocols/{protocolId}/analyses", + summary="Analyze the protocol", + description=dedent( + """ + Generate an analysis for the protocol, based on last analysis and current request data. + """ + ), + status_code=status.HTTP_201_CREATED, + responses={ + status.HTTP_200_OK: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_201_CREATED: {"model": SimpleMultiBody[AnalysisSummary]}, + status.HTTP_404_NOT_FOUND: {"model": ErrorBody[ProtocolNotFound]}, + status.HTTP_503_SERVICE_UNAVAILABLE: {"model": ErrorBody[LastAnalysisPending]}, + }, +) +async def create_protocol_analysis( + protocolId: str, + request_body: Optional[RequestModel[AnalysisRequest]] = None, + protocol_store: ProtocolStore = Depends(get_protocol_store), + analysis_store: AnalysisStore = Depends(get_analysis_store), + protocol_analyzer: ProtocolAnalyzer = Depends(get_protocol_analyzer), + task_runner: TaskRunner = Depends(get_task_runner), + analysis_id: str = Depends(get_unique_id, use_cache=False), +) -> PydanticResponse[SimpleMultiBody[AnalysisSummary]]: + """Start a new analysis for the given existing protocol. + + Starts a new analysis for the protocol along with the provided run-time parameter + values (if any), and appends it to the existing analyses. + + If the last analysis in the existing analyses used the same RTP values, then a new + analysis is not created. + + If `forceAnalyze` is True, this will always start a new analysis. + + Returns: List of analysis summaries available for the protocol, ordered as + most recently started analysis last. + """ + if not protocol_store.has(protocolId): + raise ProtocolNotFound(detail=f"Protocol {protocolId} not found").as_error( + status.HTTP_404_NOT_FOUND + ) + try: + ( + analysis_summaries, + started_new_analysis, + ) = await _start_new_analysis_if_necessary( + protocol_id=protocolId, + analysis_id=analysis_id, + rtp_values=request_body.data.runTimeParameterValues if request_body else {}, + force_reanalyze=request_body.data.forceReAnalyze if request_body else False, + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + ) + except AnalysisIsPendingError as error: + raise LastAnalysisPending(detail=str(error)).as_error( + status.HTTP_503_SERVICE_UNAVAILABLE + ) from error + return await PydanticResponse.create( + content=SimpleMultiBody.construct( + data=analysis_summaries, + meta=MultiBodyMeta(cursor=0, totalLength=len(analysis_summaries)), + ), + status_code=status.HTTP_201_CREATED + if started_new_analysis + else status.HTTP_200_OK, + ) + + @PydanticResponse.wrap_route( protocols_router.get, path="/protocols/{protocolId}/analyses", diff --git a/robot-server/robot_server/runs/dependencies.py b/robot-server/robot_server/runs/dependencies.py index 20b8d087b66d..f66ec9fdf1cd 100644 --- a/robot-server/robot_server/runs/dependencies.py +++ b/robot-server/robot_server/runs/dependencies.py @@ -43,13 +43,12 @@ async def get_run_store( app_state: AppState = Depends(get_app_state), sql_engine: SQLEngine = Depends(get_sql_engine), - runs_publisher: RunsPublisher = Depends(get_runs_publisher), ) -> RunStore: """Get a singleton RunStore to keep track of created runs.""" run_store = _run_store_accessor.get_from(app_state) if run_store is None: - run_store = RunStore(sql_engine=sql_engine, runs_publisher=runs_publisher) + run_store = RunStore(sql_engine=sql_engine) _run_store_accessor.set_on(app_state, run_store) return run_store diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index aa5b26d4a777..5b6d57520a7d 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -1,6 +1,9 @@ """In-memory storage of ProtocolEngine instances.""" -from typing import List, NamedTuple, Optional +import asyncio +import logging +from typing import List, NamedTuple, Optional, Callable +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.protocol_engine.types import PostRunHardwareState from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.robot.dev_types import RobotTypeEnum @@ -32,7 +35,13 @@ ) from robot_server.protocols.protocol_store import ProtocolResource -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import ( + DeckConfigurationType, + RunTimeParamValuesType, +) + + +_log = logging.getLogger(__name__) class EngineConflictError(RuntimeError): @@ -55,18 +64,45 @@ class RunnerEnginePair(NamedTuple): engine: ProtocolEngine -def get_estop_listener(engine_store: "EngineStore") -> HardwareEventHandler: - """Create a callback for estop events.""" +async def handle_estop_event(engine_store: "EngineStore", event: HardwareEvent) -> None: + """Handle an E-stop event from the hardware API. - def _callback(event: HardwareEvent) -> None: + This is meant to run in the engine's thread and asyncio event loop. + + This is a public function for unit-testing purposes, but it's an implementation + detail of the store. + """ + try: if isinstance(event, EstopStateNotification): if event.new_state is not EstopState.PHYSICALLY_ENGAGED: return if engine_store.current_run_id is None: return - engine_store.engine.estop(maintenance_run=False) + # todo(mm, 2024-04-17): This estop teardown sequencing belongs in the + # runner layer. + engine_store.engine.estop() + await engine_store.engine.finish(error=EStopActivatedError()) + except Exception: + # This is a background task kicked off by a hardware event, + # so there's no one to propagate this exception to. + _log.exception("Exception handling E-stop event.") + + +def _get_estop_listener(engine_store: "EngineStore") -> HardwareEventHandler: + """Create a callback for estop events. + + The returned callback is meant to run in the hardware API's thread. + """ + engine_loop = asyncio.get_running_loop() + + def run_handler_in_engine_thread_from_hardware_thread( + event: HardwareEvent, + ) -> None: + asyncio.run_coroutine_threadsafe( + handle_estop_event(engine_store, event), engine_loop + ) - return _callback + return run_handler_in_engine_thread_from_hardware_thread class EngineStore: @@ -91,7 +127,7 @@ def __init__( self._deck_type = deck_type self._default_engine: Optional[ProtocolEngine] = None self._runner_engine_pair: Optional[RunnerEnginePair] = None - hardware_api.register_callback(get_estop_listener(self)) + hardware_api.register_callback(_get_estop_listener(self)) @property def engine(self) -> ProtocolEngine: @@ -152,14 +188,19 @@ async def create( run_id: str, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], + run_time_param_values: Optional[RunTimeParamValuesType] = None, ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. Args: run_id: The run resource the engine is assigned to. labware_offsets: Labware offsets to create the engine with. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. + notify_publishers: Utilized by the engine to notify publishers of state changes. protocol: The protocol to load the runner with, if any. + run_time_param_values: Any runtime parameter values to set. Returns: The initial equipment and status summary of the engine. @@ -184,6 +225,7 @@ async def create( ), load_fixed_trash=load_fixed_trash, deck_configuration=deck_configuration, + notify_publishers=notify_publishers, ) post_run_hardware_state = PostRunHardwareState.HOME_AND_STAY_ENGAGED @@ -214,7 +256,7 @@ async def create( # was uploaded before we added stricter validation, and that # doesn't conform to the new rules. python_parse_mode=PythonParseMode.ALLOW_LEGACY_METADATA_AND_REQUIREMENTS, - run_time_param_values=None, + run_time_param_values=run_time_param_values, ) elif isinstance(runner, JsonRunner): assert ( diff --git a/robot-server/robot_server/runs/light_control_task.py b/robot-server/robot_server/runs/light_control_task.py index ee84981359a0..1cb2ad716164 100644 --- a/robot-server/robot_server/runs/light_control_task.py +++ b/robot-server/robot_server/runs/light_control_task.py @@ -32,22 +32,21 @@ def _engine_status_to_status_bar( initialization_done: bool, ) -> StatusBarState: """Convert an engine status into a status bar status.""" - if status is None: - return StatusBarState.IDLE if initialization_done else StatusBarState.OFF - - return { - EngineStatus.IDLE: StatusBarState.IDLE - if initialization_done - else StatusBarState.OFF, - EngineStatus.RUNNING: StatusBarState.RUNNING, - EngineStatus.PAUSED: StatusBarState.PAUSED, - EngineStatus.BLOCKED_BY_OPEN_DOOR: StatusBarState.PAUSED, - EngineStatus.STOP_REQUESTED: StatusBarState.UPDATING, - EngineStatus.STOPPED: StatusBarState.IDLE, - EngineStatus.FINISHING: StatusBarState.UPDATING, - EngineStatus.FAILED: StatusBarState.HARDWARE_ERROR, - EngineStatus.SUCCEEDED: StatusBarState.RUN_COMPLETED, - }[status] + match status: + case None | EngineStatus.IDLE: + return StatusBarState.IDLE if initialization_done else StatusBarState.OFF + case EngineStatus.RUNNING: + return StatusBarState.RUNNING + case EngineStatus.PAUSED | EngineStatus.AWAITING_RECOVERY | EngineStatus.BLOCKED_BY_OPEN_DOOR: + return StatusBarState.PAUSED + case EngineStatus.STOP_REQUESTED | EngineStatus.FINISHING: + return StatusBarState.UPDATING + case EngineStatus.STOPPED: + return StatusBarState.IDLE + case EngineStatus.FAILED: + return StatusBarState.HARDWARE_ERROR + case EngineStatus.SUCCEEDED: + return StatusBarState.RUN_COMPLETED def _active_updates_to_status_bar( diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index fc7b3f223e37..728966823fbe 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -5,7 +5,7 @@ import logging from datetime import datetime from textwrap import dedent -from typing import Optional, Union +from typing import Optional, Union, Callable from typing_extensions import Literal from fastapi import APIRouter, Depends, status, Query @@ -45,7 +45,7 @@ get_deck_configuration_store, ) from robot_server.deck_configuration.store import DeckConfigurationStore - +from robot_server.service.notifications import get_notify_publishers log = logging.getLogger(__name__) base_router = APIRouter() @@ -144,6 +144,7 @@ async def create_run( deck_configuration_store: DeckConfigurationStore = Depends( get_deck_configuration_store ), + notify_publishers: Callable[[], None] = Depends(get_notify_publishers), ) -> PydanticResponse[SimpleBody[Union[Run, BadRun]]]: """Create a new run. @@ -157,9 +158,13 @@ async def create_run( the new run. check_estop: Dependency to verify the estop is in a valid state. deck_configuration_store: Dependency to fetch the deck configuration. + notify_publishers: Utilized by the engine to notify publishers of state changes. """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] + rtp_values = ( + request_body.data.runTimeParameterValues if request_body is not None else None + ) protocol_resource = None deck_configuration = await deck_configuration_store.get_deck_configuration() @@ -183,7 +188,9 @@ async def create_run( created_at=created_at, labware_offsets=offsets, deck_configuration=deck_configuration, + run_time_param_values=rtp_values, protocol=protocol_resource, + notify_publishers=notify_publishers, ) except EngineConflictError as e: raise RunAlreadyActive(detail=str(e)).as_error(status.HTTP_409_CONFLICT) from e diff --git a/robot-server/robot_server/runs/run_controller.py b/robot-server/robot_server/runs/run_controller.py index 782754c1da6a..923c9cfa64ef 100644 --- a/robot-server/robot_server/runs/run_controller.py +++ b/robot-server/robot_server/runs/run_controller.py @@ -106,4 +106,5 @@ async def _run_protocol_and_insert_result( run_id=self._run_id, summary=result.state_summary, commands=result.commands, + run_time_parameters=result.parameters, ) diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 92c7d5e12b52..154a15848239 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -1,6 +1,6 @@ """Manage current and historical run data.""" from datetime import datetime -from typing import List, Optional, Union +from typing import List, Optional, Callable, Union from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.errors.exceptions import InvalidStoredData, EnumeratedError @@ -12,6 +12,7 @@ CurrentCommand, Command, ) +from opentrons.protocol_engine.types import RunTimeParamValuesType from robot_server.protocols.protocol_store import ProtocolResource from robot_server.service.task_runner import TaskRunner @@ -21,13 +22,14 @@ from .run_store import RunResource, RunStore, BadRunResource, BadStateSummary from .run_models import Run, BadRun, RunDataError -from opentrons.protocol_engine.types import DeckConfigurationType +from opentrons.protocol_engine.types import DeckConfigurationType, RunTimeParameter def _build_run( run_resource: Union[RunResource, BadRunResource], state_summary: Union[StateSummary, BadStateSummary], current: bool, + run_time_parameters: List[RunTimeParameter], ) -> Union[Run, BadRun]: # TODO(mc, 2022-05-16): improve persistence strategy # such that this default summary object is not needed @@ -48,6 +50,7 @@ def _build_run( completedAt=state_summary.completedAt, startedAt=state_summary.startedAt, liquids=state_summary.liquids, + runTimeParameters=run_time_parameters, ) errors: List[EnumeratedError] = [] @@ -101,6 +104,7 @@ def _build_run( completedAt=state.completedAt, startedAt=state.startedAt, liquids=state.liquids, + runTimeParameters=run_time_parameters, ) @@ -142,6 +146,8 @@ async def create( created_at: datetime, labware_offsets: List[LabwareOffsetCreate], deck_configuration: DeckConfigurationType, + run_time_param_values: Optional[RunTimeParamValuesType], + notify_publishers: Callable[[], None], protocol: Optional[ProtocolResource], ) -> Union[Run, BadRun]: """Create a new, current run. @@ -150,6 +156,10 @@ async def create( run_id: Identifier to assign the new run. created_at: Creation datetime. labware_offsets: Labware offsets to initialize the engine with. + deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. + notify_publishers: Utilized by the engine to notify publishers of state changes. + run_time_param_values: Any runtime parameter values to set. + protocol: The protocol to load the runner with, if any. Returns: The run resource. @@ -165,19 +175,22 @@ async def create( run_id=prev_run_id, summary=prev_run_result.state_summary, commands=prev_run_result.commands, + run_time_parameters=prev_run_result.parameters, ) state_summary = await self._engine_store.create( run_id=run_id, labware_offsets=labware_offsets, deck_configuration=deck_configuration, protocol=protocol, + run_time_param_values=run_time_param_values, + notify_publishers=notify_publishers, ) run_resource = self._run_store.insert( run_id=run_id, created_at=created_at, protocol_id=protocol.protocol_id if protocol is not None else None, ) - await self._runs_publisher.begin_polling_engine_store( + await self._runs_publisher.initialize( get_current_command=self.get_current_command, get_state_summary=self._get_good_state_summary, run_id=run_id, @@ -187,6 +200,7 @@ async def create( run_resource=run_resource, state_summary=state_summary, current=True, + run_time_parameters=[], ) def get(self, run_id: str) -> Union[Run, BadRun]: @@ -206,9 +220,10 @@ def get(self, run_id: str) -> Union[Run, BadRun]: """ run_resource = self._run_store.get(run_id=run_id) state_summary = self._get_state_summary(run_id=run_id) + parameters = self._get_run_time_parameters(run_id=run_id) current = run_id == self._engine_store.current_run_id - return _build_run(run_resource, state_summary, current) + return _build_run(run_resource, state_summary, current, parameters) def get_run_loaded_labware_definitions( self, run_id: str @@ -251,6 +266,7 @@ def get_all(self, length: Optional[int]) -> List[Union[Run, BadRun]]: run_resource=run_resource, state_summary=self._get_state_summary(run_resource.run_id), current=run_resource.run_id == self._engine_store.current_run_id, + run_time_parameters=self._get_run_time_parameters(run_resource.run_id), ) for run_resource in self._run_store.get_all(length) ] @@ -268,7 +284,7 @@ async def delete(self, run_id: str) -> None: """ if run_id == self._engine_store.current_run_id: await self._engine_store.clear() - await self._runs_publisher.stop_polling_engine_store() + await self._runs_publisher.clean_up_current_run() self._run_store.remove(run_id=run_id) @@ -301,15 +317,18 @@ async def update(self, run_id: str, current: Optional[bool]) -> Union[Run, BadRu run_id=run_id, summary=state_summary, commands=commands, + run_time_parameters=parameters, ) else: state_summary = self._engine_store.engine.state_view.get_summary() + parameters = self._engine_store.runner.run_time_parameters run_resource = self._run_store.get(run_id=run_id) return _build_run( run_resource=run_resource, state_summary=state_summary, current=next_current, + run_time_parameters=parameters, ) def get_commands_slice( @@ -376,3 +395,9 @@ def _get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary def _get_good_state_summary(self, run_id: str) -> Optional[StateSummary]: summary = self._get_state_summary(run_id) return summary if isinstance(summary, StateSummary) else None + + def _get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]: + if run_id == self._engine_store.current_run_id: + return self._engine_store.runner.run_time_parameters + else: + return self._run_store.get_run_time_parameters(run_id=run_id) diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index e05cd25330cd..c93049bfef43 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -18,6 +18,7 @@ Liquid, CommandNote, ) +from opentrons.protocol_engine.types import RunTimeParameter, RunTimeParamValuesType from opentrons_shared_data.errors import GeneralError from robot_server.service.json_api import ResourceModel from robot_server.errors.error_responses import ErrorDetails @@ -120,6 +121,15 @@ class Run(ResourceModel): ..., description="Labware offsets to apply as labware are loaded.", ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during the run." + " These are the parameters that are defined in the protocol, with values" + " specified either in the run creation request or default values from the protocol" + " if none are specified in the request." + ), + ) protocolId: Optional[str] = Field( None, description=( @@ -184,6 +194,15 @@ class BadRun(ResourceModel): ..., description="Labware offsets to apply as labware are loaded.", ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during the run." + " These are the parameters that are defined in the protocol, with values" + " specified either in the run creation request or default values from the protocol" + " if none are specified in the request." + ), + ) protocolId: Optional[str] = Field( None, description=( @@ -212,6 +231,10 @@ class RunCreate(BaseModel): default_factory=list, description="Labware offsets to apply as labware are loaded.", ) + runTimeParameterValues: Optional[RunTimeParamValuesType] = Field( + None, + description="Key-value pairs of run-time parameters defined in a protocol.", + ) class RunUpdate(BaseModel): diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 6178e180470e..b86ec8e19ea1 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -12,6 +12,7 @@ from opentrons.util.helpers import utc_now from opentrons.protocol_engine import StateSummary, CommandSlice from opentrons.protocol_engine.commands import Command +from opentrons.protocol_engine.types import RunTimeParameter from opentrons_shared_data.errors.exceptions import ( EnumeratedError, @@ -25,9 +26,13 @@ run_command_table, action_table, ) -from robot_server.persistence.pydantic import json_to_pydantic, pydantic_to_json +from robot_server.persistence.pydantic import ( + json_to_pydantic, + pydantic_to_json, + json_to_pydantic_list, + pydantic_list_to_json, +) from robot_server.protocols.protocol_store import ProtocolNotFoundError -from robot_server.service.notifications import RunsPublisher from .action_models import RunAction, RunActionType from .run_models import RunNotFoundError @@ -94,17 +99,16 @@ class RunStore: def __init__( self, sql_engine: sqlalchemy.engine.Engine, - runs_publisher: RunsPublisher, ) -> None: """Initialize a RunStore with sql engine and notification client.""" self._sql_engine = sql_engine - self._runs_publisher = runs_publisher def update_run_state( self, run_id: str, summary: StateSummary, commands: List[Command], + run_time_parameters: List[RunTimeParameter], ) -> RunResource: """Update the run's state summary and commands list. @@ -112,6 +116,7 @@ def update_run_state( run_id: The run to update summary: The run's equipment and status summary. commands: The run's commands. + run_time_parameters: The run's run time parameters, if any. Returns: The run resource. @@ -127,6 +132,7 @@ def update_run_state( run_id=run_id, state_summary=summary, engine_status=summary.status, + run_time_parameters=run_time_parameters, ) ) ) @@ -166,7 +172,6 @@ def update_run_state( action_rows = transaction.execute(select_actions).all() self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) maybe_run_resource = _convert_row_to_run(row=run_row, action_rows=action_rows) if not maybe_run_resource.ok: raise maybe_run_resource.error @@ -192,7 +197,6 @@ def insert_action(self, run_id: str, action: RunAction) -> None: transaction.execute(insert) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) def insert( self, @@ -235,7 +239,6 @@ def insert( raise ProtocolNotFoundError(protocol_id=run.protocol_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return run @lru_cache(maxsize=_CACHE_ENTRIES) @@ -352,6 +355,33 @@ def get_state_summary(self, run_id: str) -> Union[StateSummary, BadStateSummary] ) ) + @lru_cache(maxsize=_CACHE_ENTRIES) + def get_run_time_parameters(self, run_id: str) -> List[RunTimeParameter]: + """Get the archived run time parameters. + + This is a list of the run's parameter definitions (if any), + including the values used in the run itself, along with the default value, + constraints and associated names and descriptions. + """ + select_run_data = sqlalchemy.select(run_table.c.run_time_parameters).where( + run_table.c.id == run_id + ) + + with self._sql_engine.begin() as transaction: + row = transaction.execute(select_run_data).one() + + try: + return ( + json_to_pydantic_list(RunTimeParameter, row.run_time_parameters) # type: ignore[arg-type] + if row.run_time_parameters is not None + else [] + ) + except ValidationError: + log.warning( + f"Error retrieving run time parameters for {run_id}", exc_info=True + ) + return [] + def get_commands_slice( self, run_id: str, @@ -467,7 +497,6 @@ def remove(self, run_id: str) -> None: raise RunNotFoundError(run_id) self._clear_caches() - self._runs_publisher.publish_runs_advise_unsubscribe(run_id=run_id) def _run_exists( self, run_id: str, connection: sqlalchemy.engine.Connection @@ -483,6 +512,7 @@ def _clear_caches(self) -> None: self.get_all.cache_clear() self.get_state_summary.cache_clear() self.get_command.cache_clear() + self.get_run_time_parameters.cache_clear() # The columns that must be present in a row passed to _convert_row_to_run(). @@ -559,9 +589,11 @@ def _convert_state_to_sql_values( run_id: str, state_summary: StateSummary, engine_status: str, + run_time_parameters: List[RunTimeParameter], ) -> Dict[str, object]: return { "state_summary": pydantic_to_json(state_summary), "engine_status": engine_status, "_updated_at": utc_now(), + "run_time_parameters": pydantic_list_to_json(run_time_parameters), } diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index 9d2c2cb76b9d..e1e422f255cf 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -287,7 +287,7 @@ class ResponseList(BaseModel, Generic[ResponseDataT]): class NotifyRefetchBody(BaseResponseBody): """A notification response that returns a flag for refetching via HTTP.""" - refetchUsingHTTP: bool = True + refetch: bool = True class NotifyUnsubscribeBody(BaseResponseBody): diff --git a/robot-server/robot_server/service/legacy/routers/settings.py b/robot-server/robot_server/service/legacy/routers/settings.py index 16a732ff97f4..5d79053d696e 100644 --- a/robot-server/robot_server/service/legacy/routers/settings.py +++ b/robot-server/robot_server/service/legacy/routers/settings.py @@ -1,7 +1,7 @@ -from dataclasses import asdict +import aiohttp import logging +from dataclasses import asdict from typing import cast, Any, Dict, List, Optional, Union - from starlette import status from fastapi import APIRouter, Depends @@ -32,7 +32,6 @@ from robot_server.errors.error_responses import LegacyErrorResponse from robot_server.hardware import ( get_hardware, - get_robot_type, get_robot_type_enum, get_ot2_hardware, ) @@ -64,6 +63,17 @@ router = APIRouter() +# TODO: (ba, 2024-04-11): We should have a proper IPC mechanism to talk between +# the servers instead of one off endpoint calls like these. +async def set_oem_mode_request(enable): + """PUT request to set the OEM Mode for the system server.""" + async with aiohttp.ClientSession() as session: + async with session.put( + "http://127.0.0.1:31950/system/oem_mode/enable", json={"enable": enable} + ) as resp: + return resp.status + + @router.post( path="/settings", summary="Change a setting", @@ -78,10 +88,17 @@ async def post_settings( update: AdvancedSettingRequest, hardware: HardwareControlAPI = Depends(get_hardware), - robot_type: str = Depends(get_robot_type), + robot_type: RobotTypeEnum = Depends(get_robot_type_enum), ) -> AdvancedSettingsResponse: """Update advanced setting (feature flag)""" try: + # send request to system server if this is the enableOEMMode setting + if update.id == "enableOEMMode" and robot_type == RobotTypeEnum.FLEX: + resp = await set_oem_mode_request(update.value) + if resp != 200: + # TODO: raise correct error here + raise Exception(f"Something went wrong setting OEM Mode. err: {resp}") + await advanced_settings.set_adv_setting(update.id, update.value) hardware.hardware_feature_flags = HardwareFeatureFlags.build_from_ff() await hardware.set_status_bar_enabled(ff.status_bar_enabled()) @@ -104,21 +121,15 @@ async def post_settings( response_model_exclude_unset=True, ) async def get_settings( - robot_type: str = Depends(get_robot_type), + robot_type: RobotTypeEnum = Depends(get_robot_type_enum), ) -> AdvancedSettingsResponse: """Get advanced setting (feature flags)""" return _create_settings_response(robot_type) -def _create_settings_response(robot_type: str) -> AdvancedSettingsResponse: +def _create_settings_response(robot_type: RobotTypeEnum) -> AdvancedSettingsResponse: """Create the feature flag settings response object""" - # TODO lc(8-10-2023) We should convert the robot type function to return - # the enum value directly. - if robot_type == "OT-2 Standard": - robot_type_enum = RobotTypeEnum.OT2 - else: - robot_type_enum = RobotTypeEnum.FLEX - data = advanced_settings.get_all_adv_settings(robot_type_enum) + data = advanced_settings.get_all_adv_settings(robot_type) if advanced_settings.is_restart_required(): links = Links(restart="/server/restart") diff --git a/robot-server/robot_server/service/notifications/__init__.py b/robot-server/robot_server/service/notifications/__init__.py index 202c7fc71f1a..7fd648f32aa6 100644 --- a/robot-server/robot_server/service/notifications/__init__.py +++ b/robot-server/robot_server/service/notifications/__init__.py @@ -1,15 +1,20 @@ +"""Notification service creation and management.""" +from .initialize_notifications import initialize_notifications + from .notification_client import ( NotificationClient, get_notification_client, - initialize_notification_client, clean_up_notification_client, ) +from .publisher_notifier import PublisherNotifier, get_notify_publishers from .publishers import ( MaintenanceRunsPublisher, RunsPublisher, get_maintenance_runs_publisher, get_runs_publisher, ) +from .change_notifier import ChangeNotifier +from .topics import Topics __all__ = [ # main export @@ -18,10 +23,15 @@ "MaintenanceRunsPublisher", "RunsPublisher", # initialization and teardown - "initialize_notification_client", + "initialize_notifications", "clean_up_notification_client", # for use by FastAPI "get_notification_client", + "get_notify_publishers", "get_maintenance_runs_publisher", "get_runs_publisher", + # for testing + "PublisherNotifier", + "ChangeNotifier", + "Topics", ] diff --git a/robot-server/robot_server/service/notifications/change_notifier.py b/robot-server/robot_server/service/notifications/change_notifier.py new file mode 100644 index 000000000000..60c36c420af7 --- /dev/null +++ b/robot-server/robot_server/service/notifications/change_notifier.py @@ -0,0 +1,23 @@ +"""Simple state change notification interface.""" +import asyncio + + +class ChangeNotifier: + """An interface to emit or subscribe to state change notifications.""" + + def __init__(self) -> None: + """Initialize the ChangeNotifier with an internal Event.""" + self._event = asyncio.Event() + + def notify(self) -> None: + """Notify all `waiters` of a change.""" + self._event.set() + + async def wait(self) -> None: + """Wait until the next change notification.""" + self._event.clear() + await self._event.wait() + + def clear(self) -> None: + """Reset the internal event flag.""" + self._event.clear() diff --git a/robot-server/robot_server/service/notifications/initialize_notifications.py b/robot-server/robot_server/service/notifications/initialize_notifications.py new file mode 100644 index 000000000000..d5569d09efff --- /dev/null +++ b/robot-server/robot_server/service/notifications/initialize_notifications.py @@ -0,0 +1,11 @@ +"""Utilities for initializing the notification service.""" +from server_utils.fastapi_utils.app_state import AppState + +from .notification_client import initialize_notification_client +from .publisher_notifier import initialize_publisher_notifier + + +async def initialize_notifications(app_state: AppState) -> None: + """Initialize the notification system for the given app state.""" + initialize_notification_client(app_state) + await initialize_publisher_notifier(app_state) diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 568d161cf535..f53de3bbe39b 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -1,3 +1,4 @@ +"""An interface for managing interactions with the notification broker and relevant lifecycle utilities.""" import random import logging import paho.mqtt.client as mqtt @@ -58,24 +59,26 @@ def __init__( # MQTT is somewhat particular about the client_id format and will connect erratically # if an unexpected string is supplied. This clientId is derived from the paho-mqtt library. self._client_id: str = f"robot-server-{random.randint(0, 1000000)}" - self.client: mqtt.Client = mqtt.Client( + self._client: mqtt.Client = mqtt.Client( client_id=self._client_id, protocol=protocol_version ) - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect def connect(self) -> None: """Connect the client to the MQTT broker.""" - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect + self._client.on_connect = self._on_connect + self._client.on_disconnect = self._on_disconnect - self.client.connect(host=self._host, port=self._port, keepalive=self._keepalive) - self.client.loop_start() + self._client.connect( + host=self._host, port=self._port, keepalive=self._keepalive + ) + self._client.loop_start() async def disconnect(self) -> None: """Disconnect the client from the MQTT broker.""" - self.client.loop_stop() - await to_thread.run_sync(self.client.disconnect) + self._client.loop_stop() + await to_thread.run_sync(self._client.disconnect) async def publish_advise_refetch_async(self, topic: str) -> None: """Asynchronously publish a refetch message on a specific topic to the MQTT broker. @@ -104,7 +107,7 @@ def publish_advise_refetch( """ message = NotifyRefetchBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, @@ -122,7 +125,7 @@ def publish_advise_unsubscribe( """ message = NotifyUnsubscribeBody.construct() payload = message.json() - self.client.publish( + self._client.publish( topic=topic, payload=payload, qos=self._default_qos, @@ -208,7 +211,5 @@ def get_notification_client( app_state: AppState = Depends(get_app_state), ) -> Optional[NotificationClient]: """Intended to be used by endpoint functions as a FastAPI dependency.""" - notification_client: Optional[ - NotificationClient - ] = _notification_client_accessor.get_from(app_state) + notification_client = _notification_client_accessor.get_from(app_state) return notification_client diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py new file mode 100644 index 000000000000..d1769ac43798 --- /dev/null +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -0,0 +1,81 @@ +"""Provides an interface for alerting notification publishers to events and related lifecycle utilities.""" +import asyncio +from fastapi import Depends +from typing import Optional, Callable, List, Awaitable + +from server_utils.fastapi_utils.app_state import ( + AppState, + AppStateAccessor, + get_app_state, +) + +from .change_notifier import ChangeNotifier + + +class PublisherNotifier: + """An interface that invokes notification callbacks whenever a generic notify event occurs.""" + + def __init__( + self, + change_notifier: Optional[ChangeNotifier] = None, + ): + self._change_notifier = change_notifier or ChangeNotifier() + self._pe_notifier: Optional[asyncio.Task[None]] = None + self._callbacks: List[Callable[[], Awaitable[None]]] = [] + + def register_publish_callbacks( + self, callbacks: List[Callable[[], Awaitable[None]]] + ): + """Extend the list of callbacks with a given list of callbacks.""" + self._callbacks.extend(callbacks) + + async def _initialize(self) -> None: + """Initializes an instance of PublisherNotifier. This method should only be called once.""" + self._pe_notifier = asyncio.create_task(self._wait_for_event()) + + def _notify_publishers(self) -> None: + """A generic notifier, alerting all `waiters` of a change.""" + self._change_notifier.notify() + + async def _wait_for_event(self) -> None: + """Indefinitely wait for an event to occur, then invoke each callback.""" + while True: + await self._change_notifier.wait() + for callback in self._callbacks: + await callback() + + +_publisher_notifier_accessor: AppStateAccessor[PublisherNotifier] = AppStateAccessor[ + PublisherNotifier +]("publisher_notifier") + + +def get_publisher_notifier( + app_state: AppState = Depends(get_app_state), +) -> PublisherNotifier: + """Intended for use by various publishers only.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert publisher_notifier is not None + + return publisher_notifier + + +def get_notify_publishers( + app_state: AppState = Depends(get_app_state), +) -> Callable[[], None]: + """Provides access to the callback used to notify publishers of changes.""" + publisher_notifier = _publisher_notifier_accessor.get_from(app_state) + assert isinstance(publisher_notifier, PublisherNotifier) + + return publisher_notifier._notify_publishers + + +async def initialize_publisher_notifier(app_state: AppState) -> None: + """Create a new `NotificationClient` and store it on `app_state`. + + Intended to be called just once, when the server starts up. + """ + publisher_notifier: PublisherNotifier = PublisherNotifier() + _publisher_notifier_accessor.set_on(app_state, publisher_notifier) + + await publisher_notifier._initialize() diff --git a/robot-server/robot_server/service/notifications/publishers/__init__.py b/robot-server/robot_server/service/notifications/publishers/__init__.py index 1dcdc43d4a95..59a30e7a1356 100644 --- a/robot-server/robot_server/service/notifications/publishers/__init__.py +++ b/robot-server/robot_server/service/notifications/publishers/__init__.py @@ -1,3 +1,8 @@ +"""Publisher creation and management. + +A unique publisher is responsible for each router's related set of endpoints. The publisher conditionally determines +whether a relevant event has occurred, and if true, it publishes an appropriate message to the robot's message broker. +""" from .maintenance_runs_publisher import ( MaintenanceRunsPublisher, get_maintenance_runs_publisher, diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index 94aed694e8f1..b6744fbc90a5 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -1,7 +1,7 @@ -from fastapi import Depends import asyncio -import logging -from typing import Union, Callable, Optional +from fastapi import Depends +from dataclasses import dataclass +from typing import Callable, Optional from opentrons.protocol_engine import CurrentCommand, StateSummary, EngineStatus @@ -11,173 +11,120 @@ get_app_state, ) from ..notification_client import NotificationClient, get_notification_client +from ..publisher_notifier import PublisherNotifier, get_publisher_notifier from ..topics import Topics -log: logging.Logger = logging.getLogger(__name__) +@dataclass +class RunHooks: + """Generated during a protocol run. Utilized by RunsPublisher.""" + + run_id: str + get_current_command: Callable[[str], Optional[CurrentCommand]] + get_state_summary: Callable[[str], Optional[StateSummary]] + + +@dataclass +class EngineStateSlice: + """Protocol Engine state relevant to RunsPublisher.""" -POLL_INTERVAL = 1 + current_command: Optional[CurrentCommand] = None + state_summary_status: Optional[EngineStatus] = None class RunsPublisher: """Publishes protocol runs topics.""" - def __init__(self, client: NotificationClient) -> None: + def __init__( + self, client: NotificationClient, publisher_notifier: PublisherNotifier + ) -> None: """Returns a configured Runs Publisher.""" self._client = client + self._publisher_notifier = publisher_notifier self._run_data_manager_polling = asyncio.Event() - self._previous_current_command: Union[CurrentCommand, None] = None - self._previous_state_summary_status: Union[EngineStatus, None] = None self._poller: Optional[asyncio.Task[None]] = None + # Variables and callbacks related to PE state changes. + self._run_hooks: Optional[RunHooks] = None + self._engine_state_slice: Optional[EngineStateSlice] = None - # TODO(jh, 2023-02-02): Instead of polling, emit current_commands directly from PE. - async def begin_polling_engine_store( - self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ) -> None: - """Continuously poll the engine store for the current_command. - - Args: - get_current_command: Callback to get the currently executing command, if any. - get_state_summary: Callback to get the current run's state summary, if any. - run_id: ID of the current run. - """ - if self._poller is None: - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - else: - await self.stop_polling_engine_store() - self._poller = asyncio.create_task( - self._poll_engine_store( - get_current_command=get_current_command, - run_id=run_id, - get_state_summary=get_state_summary, - ) - ) - - async def stop_polling_engine_store(self) -> None: - """Stops polling the engine store. Run-related topics will publish as the poller is cancelled.""" - if self._poller is not None: - self._run_data_manager_polling.set() - self._poller.cancel() - - def publish_runs_advise_refetch(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_refetch(topic=Topics.RUNS) - self._client.publish_advise_refetch(topic=f"{Topics.RUNS}/{run_id}") - - def publish_runs_advise_unsubscribe(self, run_id: str) -> None: - """Publishes the equivalent of GET /runs and GET /runs/:runId. - - Args: - run_id: ID of the current run. - """ - self._client.publish_advise_unsubscribe(topic=Topics.RUNS) - self._client.publish_advise_unsubscribe(topic=f"{Topics.RUNS}/{run_id}") + self._publisher_notifier.register_publish_callbacks( + [self._handle_current_command_change, self._handle_engine_status_change] + ) - async def _poll_engine_store( + async def initialize( self, - get_current_command: Callable[[str], Optional[CurrentCommand]], - get_state_summary: Callable[[str], Optional[StateSummary]], run_id: str, - ) -> None: - """Asynchronously publish new current commands. - - Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. - run_id: ID of the current run. - """ - try: - await self._poll_for_run_id_info( - get_current_command=get_current_command, - get_state_summary=get_state_summary, - run_id=run_id, - ) - except asyncio.CancelledError: - self._clean_up_poller() - await self._publish_runs_advise_unsubscribe_async(run_id=run_id) - await self._client.publish_advise_refetch_async( - topic=Topics.RUNS_CURRENT_COMMAND - ) - except Exception as e: - log.error(f"Error within run data manager poller: {e}") - - async def _poll_for_run_id_info( - self, get_current_command: Callable[[str], Optional[CurrentCommand]], get_state_summary: Callable[[str], Optional[StateSummary]], - run_id: str, - ): - """Poll the engine store for a specific run's state while the poll is active. + ) -> None: + """Initialize RunsPublisher with necessary information derived from the current run. Args: - get_current_command: Retrieves the engine store's current command. - get_state_summary: Retrieves the engine store's state summary. run_id: ID of the current run. + get_current_command: Callback to get the currently executing command, if any. + get_state_summary: Callback to get the current run's state summary, if any. """ + self._run_hooks = RunHooks( + run_id=run_id, + get_current_command=get_current_command, + get_state_summary=get_state_summary, + ) + self._engine_state_slice = EngineStateSlice() - while not self._run_data_manager_polling.is_set(): - current_command = get_current_command(run_id) - current_state_summary = get_state_summary(run_id) - current_state_summary_status = ( - current_state_summary.status if current_state_summary else None - ) - - if self._previous_current_command != current_command: - await self._publish_current_command() - self._previous_current_command = current_command + await self._publish_runs_advise_refetch_async() - if self._previous_state_summary_status != current_state_summary_status: - await self._publish_runs_advise_refetch_async(run_id=run_id) - self._previous_state_summary_status = current_state_summary_status - await asyncio.sleep(POLL_INTERVAL) + async def clean_up_current_run(self) -> None: + """Publish final refetch and unsubscribe flags.""" + await self._publish_runs_advise_refetch_async() + await self._publish_runs_advise_unsubscribe_async() - async def _publish_current_command( - self, - ) -> None: + async def _publish_current_command(self) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" await self._client.publish_advise_refetch_async( topic=Topics.RUNS_CURRENT_COMMAND ) - async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via a refetch message. + async def _publish_runs_advise_refetch_async(self) -> None: + """Publish a refetch flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + await self._client.publish_advise_refetch_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_refetch_async(topic=Topics.RUNS) - await self._client.publish_advise_refetch_async(topic=f"{Topics.RUNS}/{run_id}") + async def _publish_runs_advise_unsubscribe_async(self) -> None: + """Publish an unsubscribe flag for relevant runs topics.""" + if self._run_hooks is not None: + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{self._run_hooks.run_id}" + ) - async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via an unsubscribe message. + async def _handle_current_command_change(self) -> None: + """Publish a refetch flag if the current command has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_command = self._run_hooks.get_current_command( + self._run_hooks.run_id + ) + if self._engine_state_slice.current_command != current_command: + await self._publish_current_command() + self._engine_state_slice.current_command = current_command - Args: - run_id: ID of the current run. - """ - await self._client.publish_advise_unsubscribe_async(topic=Topics.RUNS) - await self._client.publish_advise_unsubscribe_async( - topic=f"{Topics.RUNS}/{run_id}" - ) + async def _handle_engine_status_change(self) -> None: + """Publish a refetch flag if the engine status has changed.""" + if self._run_hooks is not None and self._engine_state_slice is not None: + current_state_summary = self._run_hooks.get_state_summary( + self._run_hooks.run_id + ) - def _clean_up_poller(self) -> None: - """Cleans up the runs data manager poller.""" - self._poller = None - self._run_data_manager_polling.clear() - self._previous_current_command = None - self._previous_state_summary_status = None + if ( + current_state_summary is not None + and self._engine_state_slice.state_summary_status + != current_state_summary.status + ): + await self._publish_runs_advise_refetch_async() + self._engine_state_slice.state_summary_status = ( + current_state_summary.status + ) _runs_publisher_accessor: AppStateAccessor[RunsPublisher] = AppStateAccessor[ @@ -188,12 +135,15 @@ def _clean_up_poller(self) -> None: async def get_runs_publisher( app_state: AppState = Depends(get_app_state), notification_client: NotificationClient = Depends(get_notification_client), + publisher_notifier: PublisherNotifier = Depends(get_publisher_notifier), ) -> RunsPublisher: """Get a singleton RunsPublisher to publish runs topics.""" runs_publisher = _runs_publisher_accessor.get_from(app_state) if runs_publisher is None: - runs_publisher = RunsPublisher(client=notification_client) + runs_publisher = RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) _runs_publisher_accessor.set_on(app_state, runs_publisher) return runs_publisher diff --git a/robot-server/robot_server/service/notifications/topics.py b/robot-server/robot_server/service/notifications/topics.py index 9e3d5fe0ea45..34f2fd0eea14 100644 --- a/robot-server/robot_server/service/notifications/topics.py +++ b/robot-server/robot_server/service/notifications/topics.py @@ -1,3 +1,4 @@ +"""Notification topics.""" from enum import Enum diff --git a/robot-server/simulators/test-flex.json b/robot-server/simulators/test-flex.json index d7fc860c6623..adc9543fc5ac 100644 --- a/robot-server/simulators/test-flex.json +++ b/robot-server/simulators/test-flex.json @@ -91,4 +91,4 @@ } ] } -} \ No newline at end of file +} diff --git a/robot-server/tests/deck_configuration/test_defaults.py b/robot-server/tests/deck_configuration/test_defaults.py index ec3bbed3c22c..42aa3672f521 100644 --- a/robot-server/tests/deck_configuration/test_defaults.py +++ b/robot-server/tests/deck_configuration/test_defaults.py @@ -12,7 +12,7 @@ from robot_server.deck_configuration import validation_mapping -DECK_DEFINITION_VERSION: Final = 4 +DECK_DEFINITION_VERSION: Final = 5 @pytest.mark.parametrize( diff --git a/robot-server/tests/deck_configuration/test_validation.py b/robot-server/tests/deck_configuration/test_validation.py index 5aee74491da8..24aecf9117a4 100644 --- a/robot-server/tests/deck_configuration/test_validation.py +++ b/robot-server/tests/deck_configuration/test_validation.py @@ -7,22 +7,26 @@ def test_valid() -> None: """It should return an empty error list if the input is valid.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == set() @@ -30,23 +34,27 @@ def test_valid() -> None: def test_invalid_empty_cutouts() -> None: """It should enforce that every cutout is occupied.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), # Invalid because we haven't placed anything into cutout C2 or D2. - # ("singleCenterSlot", "cutoutC2"), - # ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + # ("singleCenterSlot", "cutoutC2", None), + # ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -57,26 +65,30 @@ def test_invalid_empty_cutouts() -> None: def test_invalid_overcrowded_cutouts() -> None: """It should prevent you from putting multiple things into a single cutout.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), # Invalid because we're placing two things in cutout C3... - ("stagingAreaRightSlot", "cutoutC3"), - ("stagingAreaRightSlot", "cutoutC3"), + ("stagingAreaRightSlot", "cutoutC3", None), + ("stagingAreaRightSlot", "cutoutC3", None), # ...and two things in cutout D3. - ("wasteChuteRightAdapterNoCover", "cutoutD3"), - ("singleRightSlot", "cutoutD3"), + ("wasteChuteRightAdapterNoCover", "cutoutD3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -93,24 +105,28 @@ def test_invalid_overcrowded_cutouts() -> None: def test_invalid_cutout_for_fixture() -> None: """Each fixture must be placed in a location that's valid for that particular fixture.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), # Invalid because wasteChuteRightAdapterNoCover can't be placed in cutout C2... - ("wasteChuteRightAdapterNoCover", "cutoutC2"), + ("wasteChuteRightAdapterNoCover", "cutoutC2", None), # ...nor can singleLeftSlot be placed in cutout D2. - ("singleLeftSlot", "cutoutD2"), - ("stagingAreaRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("stagingAreaRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + ("singleLeftSlot", "cutoutD2", None), + ("stagingAreaRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("stagingAreaRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -131,24 +147,28 @@ def test_invalid_cutout_for_fixture() -> None: def test_unrecognized_cutout() -> None: """It should raise a sensible error if you pass a totally nonexistent cutout.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("singleRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("singleRightSlot", "cutoutC3"), - ("singleRightSlot", "cutoutD3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), # Invalid because "someUnrecognizedCutout" is not defined by the deck definition. - ("singleRightSlot", "someUnrecognizedCutout"), + ("singleRightSlot", "someUnrecognizedCutout", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -164,23 +184,27 @@ def test_unrecognized_cutout() -> None: def test_unrecognized_cutout_fixture() -> None: """It should raise a sensible error if you pass a totally nonexistent cutout fixture.""" - deck_definition = load_deck_definition("ot3_standard", version=4) + deck_definition = load_deck_definition("ot3_standard", version=5) cutout_fixtures = [ - subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) - for cutout_fixture_id, cutout_id in [ - ("singleLeftSlot", "cutoutA1"), - ("singleLeftSlot", "cutoutB1"), - ("singleLeftSlot", "cutoutC1"), - ("singleLeftSlot", "cutoutD1"), - ("singleCenterSlot", "cutoutA2"), - ("singleCenterSlot", "cutoutB2"), - ("singleCenterSlot", "cutoutC2"), - ("singleCenterSlot", "cutoutD2"), - ("singleRightSlot", "cutoutA3"), - ("singleRightSlot", "cutoutB3"), - ("singleRightSlot", "cutoutC3"), + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("singleLeftSlot", "cutoutA1", None), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), # Invalid because "someUnrecognizedCutoutFixture" is not defined by the deck definition. - ("someUnrecognizedCutoutFixture", "cutoutD3"), + ("someUnrecognizedCutoutFixture", "cutoutD3", None), ] ] assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { @@ -197,7 +221,115 @@ def test_unrecognized_cutout_fixture() -> None: "stagingAreaSlotWithWasteChuteRightAdapterCovered", "stagingAreaSlotWithWasteChuteRightAdapterNoCover", "trashBinAdapter", + "thermocyclerModuleV2Rear", + "thermocyclerModuleV2Front", + "heaterShakerModuleV1", + "temperatureModuleV2", + "magneticBlockV1", + "stagingAreaSlotWithMagneticBlockV1", ] ), ) } + + +def test_invalid_serial_number() -> None: + """It should raise a sensible error if you fail to provide a serial number for a fixture that requires one.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("thermocyclerModuleV2Rear", "cutoutA1", "ABC"), + # Invalid, because the Thermocycler V2 Front fixture requires a serial number + ("thermocyclerModuleV2Front", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.InvalidSerialNumberError( + cutout_fixture_id="thermocyclerModuleV2Front", + cutout_id="cutoutB1", + ) + } + + +def test_unexpected_serial_number() -> None: + """It should raise a sensible error if you provide a serial number for a fixture that DOES NOT require one.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + # Invalid, single slot fixtures do not have serial numbers + ("singleLeftSlot", "cutoutA1", "ABC"), + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.UnexpectedSerialNumberError( + cutout_fixture_id="singleLeftSlot", + cutout_id="cutoutA1", + opentrons_module_serial_number="ABC", + ) + } + + +# new test to raise error if not all members of a fixture group are loaded into the deck config +def test_missing_group_fixture() -> None: + """It should raise a sensible error if you fail to provide all members of a fixture group in a deck configuration.""" + deck_definition = load_deck_definition("ot3_standard", version=5) + cutout_fixtures = [ + subject.Placement( + cutout_fixture_id=cutout_fixture_id, + cutout_id=cutout_id, + opentrons_module_serial_number=opentrons_module_serial_number, + ) + for cutout_fixture_id, cutout_id, opentrons_module_serial_number in [ + ("thermocyclerModuleV2Rear", "cutoutA1", "ABC"), + # Invalid, because the Thermocycler V2 Rear fixture above requires a Front fixture be loaded as well + ("singleLeftSlot", "cutoutB1", None), + ("singleLeftSlot", "cutoutC1", None), + ("singleLeftSlot", "cutoutD1", None), + ("singleCenterSlot", "cutoutA2", None), + ("singleCenterSlot", "cutoutB2", None), + ("singleCenterSlot", "cutoutC2", None), + ("singleCenterSlot", "cutoutD2", None), + ("singleRightSlot", "cutoutA3", None), + ("singleRightSlot", "cutoutB3", None), + ("singleRightSlot", "cutoutC3", None), + ("singleRightSlot", "cutoutD3", None), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.MissingGroupFixtureError( + cutout_fixture_id="thermocyclerModuleV2Rear", + cutout_id="cutoutA1", + missing_fixture_id="thermocyclerModuleV2Front", + ) + } diff --git a/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml b/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml index 8e4e99528a79..c9cc22f8e0df 100644 --- a/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml +++ b/robot-server/tests/integration/http_api/commands/test_load_module_success.tavern.yaml @@ -49,7 +49,7 @@ stages: params: model: '{model}' location: - slotName: '10' + slotName: '7' response: strict: - json:off diff --git a/robot-server/tests/integration/http_api/persistence/test_reset.py b/robot-server/tests/integration/http_api/persistence/test_reset.py index c9973713802d..394671bba642 100644 --- a/robot-server/tests/integration/http_api/persistence/test_reset.py +++ b/robot-server/tests/integration/http_api/persistence/test_reset.py @@ -40,9 +40,9 @@ async def _assert_reset_was_successful( all_files_and_directories = set(persistence_directory.glob("**/*")) expected_files_and_directories = { persistence_directory / "robot_server.db", - persistence_directory / "3", - persistence_directory / "3" / "protocols", - persistence_directory / "3" / "robot_server.db", + persistence_directory / "4", + persistence_directory / "4" / "protocols", + persistence_directory / "4" / "robot_server.db", } assert all_files_and_directories == expected_files_and_directories diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml index 3634989ed3f8..0451b3eebc45 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses.tavern.yaml @@ -85,25 +85,21 @@ stages: Content-Type: application/json json: !force_format_include '{analysis_data}' - - name: Check that uploading the same protocol with run-time parameter values triggers re-analysis - # This test must be executed after the analysis of the previous upload is completed. + + - name: Check that a new analysis is started with forceReAnalyze request: - url: '{ot2_server_base_url}/protocols' + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' method: POST - data: - runTimeParameterValues: '{{"volume": 123, "dry_run": true, "pipette": "p10_single"}}' - files: - files: 'tests/integration/protocols/basic_transfer_standalone.py' + json: + data: + forceReAnalyze: true response: strict: - json:off - status_code: 200 + status_code: 201 json: data: - id: '{protocol_id}' - analyses: [] - analysisSummaries: - - id: '{analysis_id}' - status: completed - - id: !anystr - status: pending + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml new file mode 100644 index 000000000000..fa37eadc20c0 --- /dev/null +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_run_time_parameters.tavern.yaml @@ -0,0 +1,201 @@ +test_name: Test the protocol analysis endpoints with run time parameters + +marks: + - usefixtures: + - ot2_server_base_url + +stages: + - name: Upload a protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + protocol_id: data.id + analysis_id: data.analysisSummaries[0].id + strict: + - json:off + status_code: 201 + json: + data: + analyses: [] + analysisSummaries: + - id: !anystr + status: pending + + - name: Check that the analysis summary is present in /protocols/:id; retry until it says it's completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}' + response: + status_code: 200 + json: + data: + analyses: [] + analysisSummaries: + - id: '{analysis_id}' + status: completed + id: !anything + protocolType: !anything + files: !anything + createdAt: !anything + robotType: !anything + metadata: !anything + links: !anything + + - name: Check that the analysis data is present in /protocols/:id/analyses/:id + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 6.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 20.1 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: false + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette + + - name: Check that uploading same protocol with new run time parameter values re-triggers analysis + # This test must be executed after the analysis of the previous upload is completed. + request: + url: '{ot2_server_base_url}/protocols' + method: POST + data: + runTimeParameterValues: '{{"sample_count": 10, "volume": 10.23, "dry_run": true}}' + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + save: + json: + analysis_id2: data.analysisSummaries[1].id + strict: + - json:off + status_code: 200 + json: + data: + id: '{protocol_id}' + analyses: [ ] + analysisSummaries: + - id: '{analysis_id}' + status: completed + - id: !anystr + status: pending + + - name: Check that the new analysis uses run time parameter values from client; retry until analysis is completed + max_retries: 5 + delay_after: 1 + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses/{analysis_id2}' + response: + strict: + - json:off + json: + data: + id: '{analysis_id2}' + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 10.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_1channel_50 + description: What pipette to use during the protocol. + commands: + # Check for this command's presence as a smoke test that the analysis isn't empty. + - commandType: loadPipette + + - name: Check that a new analysis is started for the protocol because of new RTP values + request: + url: '{ot2_server_base_url}/protocols/{protocol_id}/analyses' + method: POST + json: + data: + runTimeParameterValues: + sample_count: 2 + response: + strict: + - json:off + status_code: 201 + json: + data: + - id: '{analysis_id}' + status: completed + - id: '{analysis_id2}' + status: completed + - id: !anystr + status: pending \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml index 7d0f4361cb39..7729ee15fa56 100644 --- a/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_key.tavern.yaml @@ -169,6 +169,8 @@ stages: author: engineer@opentrons.com key: duplicate_key - name: Upload basic_transfer_standalone protocol with same key + # add a delay before starting to let previous analysis complete + delay_before: 2 request: url: '{ot2_server_base_url}/protocols' method: POST diff --git a/robot-server/tests/integration/http_api/protocols/test_persistence.py b/robot-server/tests/integration/http_api/protocols/test_persistence.py index a939f5f5fdad..0480accb39c1 100644 --- a/robot-server/tests/integration/http_api/protocols/test_persistence.py +++ b/robot-server/tests/integration/http_api/protocols/test_persistence.py @@ -120,10 +120,10 @@ async def test_protocol_labware_files_persist() -> None: assert restarted_protocol_detail == protocol_detail four_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_4_tuberack_100ul.json" ) six_tuberack = Path( - f"{server.persistence_directory}/3/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" + f"{server.persistence_directory}/4/protocols/{protocol_id}/cpx_6_tuberack_100ul.json" ) assert four_tuberack.is_file() assert six_tuberack.is_file() diff --git a/robot-server/tests/integration/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 991d88df87f2..af2fc892b862 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -103,7 +103,7 @@ stages: definitionUri: opentrons/armadillo_96_wellplate_200ul_pcr_full_skirt/1 displayName: Sample Collection Plate location: - moduleId: magneticModuleId + moduleId: magneticBlockId - id: tipRackId loadName: opentrons_96_tiprack_1000ul definitionUri: opentrons/opentrons_96_tiprack_1000ul/1 @@ -117,9 +117,8 @@ stages: location: slotName: 'A3' modules: - - id: magneticModuleId - serialNumber: !anystr - model: magneticModuleV2 + - id: magneticBlockId + model: magneticBlockV1 location: slotName: 'D3' - id: temperatureModuleId @@ -159,15 +158,14 @@ stages: key: !anystr status: succeeded params: - model: magneticModuleV2 + model: magneticBlockV1 location: slotName: 'D3' - moduleId: magneticModuleId + moduleId: magneticBlockId result: - moduleId: magneticModuleId + moduleId: magneticBlockId definition: !anydict - model: magneticModuleV2 - serialNumber: !anystr + model: magneticBlockV1 notes: [] startedAt: !anystr completedAt: !anystr @@ -215,7 +213,7 @@ stages: status: succeeded params: location: - moduleId: magneticModuleId + moduleId: magneticBlockId loadName: armadillo_96_wellplate_200ul_pcr_full_skirt namespace: opentrons version: 1 @@ -365,7 +363,7 @@ stages: volume: 4.5 flowRate: 2.5 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 84.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 40.05 } volume: 4.5 notes: [] startedAt: !anystr @@ -388,7 +386,7 @@ stages: radius: 1.0 speed: 42.0 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 94.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 50.05 } notes: [] startedAt: !anystr completedAt: !anystr @@ -409,7 +407,7 @@ stages: z: 12.0 flowRate: 2.0 result: - position: { 'x': 341.205, 'y': 65.115, 'z': 95.3 } + position: { 'x': 342.38, 'y': 65.24, 'z': 51.05 } notes: [] startedAt: !anystr completedAt: !anystr @@ -448,7 +446,8 @@ stages: forceDirect: false speed: 12.3 result: - position: { 'x': 350.205, 'y': 65.115, 'z': 98.25 } + position: + { 'x': 351.38, 'y': 65.24, 'z': 54.0 } notes: [] startedAt: !anystr completedAt: !anystr @@ -470,7 +469,8 @@ stages: minimumZHeight: 35.0 forceDirect: true result: - position: { 'x': 352.205, 'y': 68.115, 'z': 93.3 } + position: + { 'x': 353.38, 'y': 68.24, 'z': 49.05 } notes: [] startedAt: !anystr completedAt: !anystr diff --git a/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py b/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py index aa39376cafe5..4f065b9a59c2 100644 --- a/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py +++ b/robot-server/tests/integration/http_api/runs/test_deck_slot_standardization.py @@ -4,6 +4,7 @@ from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] from ...robot_client import RobotClient +from ...conftest import _OT3_SESSION_SERVER_PORT @pytest.fixture @@ -58,6 +59,29 @@ async def test_deck_slot_standardization( """ module_model = "temperatureModuleV2" + deck_configuration_request = [ + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutA1"}, + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutB1"}, + {"cutoutFixtureId": "singleLeftSlot", "cutoutId": "cutoutC1"}, + { + "cutoutFixtureId": "temperatureModuleV2", + "cutoutId": "cutoutD1", + "opentronsModuleSerialNumber": "temp-1234", + }, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutA2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutB2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutC2"}, + {"cutoutFixtureId": "singleCenterSlot", "cutoutId": "cutoutD2"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutA3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutB3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutC3"}, + {"cutoutFixtureId": "singleRightSlot", "cutoutId": "cutoutD3"}, + ] + if _OT3_SESSION_SERVER_PORT in robot_client.base_url: + await robot_client.put_deck_configuration( + req_body={"data": {"cutoutFixtures": deck_configuration_request}} + ) + labware_load_name = "armadillo_96_wellplate_200ul_pcr_full_skirt" labware_namespace = "opentrons" labware_version = 1 diff --git a/robot-server/tests/integration/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 1e7d7e20be42..e7ac3483dd7c 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -50,6 +50,7 @@ stages: displayName: Water description: Liquid H2O displayColor: '#7332a8' + runTimeParameters: [] protocolId: '{protocol_id}' - name: Execute a setup command @@ -93,10 +94,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -105,184 +106,6 @@ stages: createdAt: !anystr status: queued params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -352,6 +175,16 @@ stages: params: {} startedAt: !anystr completedAt: !anystr + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } + notes: [] - id: !anystr key: !anystr commandType: loadPipette @@ -569,16 +402,6 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} - name: Verify commands succeeded with pageLength and cursor request: @@ -610,12 +433,12 @@ stages: notes: [] params: location: - moduleId: magneticModuleId + moduleId: temperatureModuleId loadName: foo_8_plate_33ul namespace: example version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate + labwareId: sourcePlateId + displayName: Source Plate - id: !anystr key: !anystr commandType: loadLabware @@ -626,9 +449,9 @@ stages: notes: [] params: location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons + moduleId: magneticModuleId + loadName: foo_8_plate_33ul + namespace: example version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL + labwareId: destPlateId + displayName: Sample Collection Plate diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index 46eccbae2809..80c7f1b2ef52 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -86,12 +86,12 @@ stages: meta: runId: !anystr commandId: !anystr - index: 4 + index: 3 key: !anystr createdAt: !anystr meta: cursor: 3 - totalLength: 5 + totalLength: 4 data: - id: !anystr key: !anystr @@ -120,20 +120,4 @@ stages: y: 0 z: 1 flowRate: 3.78 - volume: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - completedAt: !anystr - status: failed - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: A1 - wellLocation: - origin: top - offset: - x: 0 - y: 0 - z: 0 + volume: 100 \ No newline at end of file diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 089b5f30c032..bdc4ad4a66d7 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -45,6 +45,7 @@ stages: definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1 location: !anydict labwareOffsets: [] + runTimeParameters: [] liquids: - id: waterId displayName: Water @@ -93,10 +94,10 @@ stages: commandId: '{setup_command_id}' key: '{setup_command_key}' createdAt: '{setup_command_created_at}' - index: 14 + index: 1 meta: cursor: 0 - totalLength: 15 + totalLength: 2 data: # Initial home - id: !anystr @@ -104,185 +105,7 @@ stages: commandType: home createdAt: !anystr status: queued - params: {} - - id: !anystr - key: !anystr - commandType: loadPipette - createdAt: !anystr - status: queued - params: - pipetteName: p10_single - mount: left - pipetteId: pipetteId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: magneticModuleV1 - location: - slotName: '3' - moduleId: magneticModuleId - - id: !anystr - key: !anystr - commandType: loadModule - createdAt: !anystr - status: queued - params: - model: temperatureModuleV2 - location: - slotName: '1' - moduleId: temperatureModuleId - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: temperatureModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: sourcePlateId - displayName: Source Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - moduleId: magneticModuleId - loadName: foo_8_plate_33ul - namespace: example - version: 1 - labwareId: destPlateId - displayName: Sample Collection Plate - - id: !anystr - key: !anystr - commandType: loadLabware - createdAt: !anystr - status: queued - params: - location: - slotName: '8' - loadName: opentrons_96_tiprack_10ul - namespace: opentrons - version: 1 - labwareId: tipRackId - displayName: Opentrons 96 Tip Rack 10 µL - - id: !anystr - createdAt: !anystr - commandType: loadLiquid - key: !anystr - status: queued - params: - liquidId: 'waterId' - labwareId: 'sourcePlateId' - volumeByWell: - A1: 100 - B1: 100 - - id: !anystr - key: !anystr - commandType: pickUpTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: tipRackId - wellName: B1 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - - id: !anystr - key: !anystr - commandType: aspirate - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: sourcePlateId - wellName: A1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 2 - volume: 5 - flowRate: 3 - - id: !anystr - key: !anystr - commandType: dispense - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B1 - wellLocation: - origin: bottom - offset: - x: 0 - 'y': 0 - z: 1 - volume: 4.5 - flowRate: 2.5 - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: top - offset: - x: 0 - 'y': 0 - z: 0 - forceDirect: false - - id: !anystr - key: !anystr - commandType: moveToWell - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: destPlateId - wellName: B2 - wellLocation: - origin: bottom - offset: - x: 2 - y: 3 - z: 10 - minimumZHeight: 35 - forceDirect: true - speed: 12.3 - - id: !anystr - key: !anystr - commandType: dropTip - createdAt: !anystr - status: queued - params: - pipetteId: pipetteId - labwareId: fixedTrash - wellName: A1 - wellLocation: - origin: default - offset: - x: 0 - y: 0 - z: 0 - alternateDropLocation: false + params: { } - id: '{setup_command_id}' key: '{setup_command_key}' intent: setup @@ -350,8 +173,18 @@ stages: startedAt: !anystr completedAt: !anystr status: succeeded + params: { } + notes: [ ] + - id: '{setup_command_id}' + key: '{setup_command_key}' + intent: setup + commandType: home + createdAt: '{setup_command_created_at}' + startedAt: '{setup_command_started_at}' + completedAt: '{setup_command_completed_at}' + status: succeeded + params: { } notes: [] - params: {} - id: !anystr key: !anystr commandType: loadPipette @@ -569,13 +402,3 @@ stages: y: 0 z: 0 alternateDropLocation: false - - id: '{setup_command_id}' - key: '{setup_command_key}' - intent: setup - commandType: home - createdAt: '{setup_command_created_at}' - startedAt: '{setup_command_started_at}' - completedAt: '{setup_command_completed_at}' - status: succeeded - notes: [] - params: {} diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml new file mode 100644 index 000000000000..d59b533ca672 --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_papi.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test python protocol run commands are failed when stopped. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a python protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_papi.py' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml new file mode 100644 index 000000000000..e3d6d5b659f7 --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_play_stop_v6.tavern.yaml @@ -0,0 +1,128 @@ +test_name: Test a JSONv6 run can be paused and then cancelled. + +marks: + - usefixtures: + - ot2_server_base_url +stages: + - name: Upload a JSONv6 protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/wait_for_resume_stop_v6.json' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + response: + status_code: 201 + save: + json: + run_id: data.id + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + + - name: Wait for the command to run + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - commandType: waitForDuration + status: running + + - name: Stop the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: stop + response: + status_code: 201 + + - name: Wait for the run to complete + max_retries: 10 + delay_after: 0.2 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: stopped + + - name: Get run commands + request: + url: '{ot2_server_base_url}/runs/{run_id}/commands' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: {} + notes: [] + - id: !anystr + key: !anystr + commandType: home + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: succeeded + params: { } + notes: [ ] + - id: !anystr + key: !anystr + commandType: waitForDuration + createdAt: !anystr + startedAt: !anystr + completedAt: !anystr + status: failed + params: + seconds: 30 + notes: [ ] + error: + createdAt: !anystr + detail: 'Run was cancelled' + errorCode: '4000' + errorInfo: { } + errorType: 'RunStoppedError' + id: !anystr + wrappedErrors: [ ] + + diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index 48dc570d6c97..67d1511a6663 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -42,6 +42,7 @@ stages: definitionUri: opentrons/opentrons_1_trash_1100ml_fixed/1 location: !anydict labwareOffsets: [] + runTimeParameters: [] protocolId: '{protocol_id}' liquids: [] save: @@ -237,6 +238,7 @@ stages: createdAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" startedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" liquids: [] + runTimeParameters: [] completedAt: !re_search "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+\\+\\d{2}:\\d{2}$" errors: [] pipettes: [] diff --git a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml index cc8cea69356a..0d4a00102810 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml @@ -94,6 +94,7 @@ stages: labware: [] labwareOffsets: [] liquids: [] + runTimeParameters: [] modules: [] pipettes: [] status: 'idle' diff --git a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml new file mode 100644 index 000000000000..d7f075b18cb3 --- /dev/null +++ b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml @@ -0,0 +1,203 @@ +test_name: Test the run endpoints with run time parameters + +marks: + - usefixtures: + - ot2_server_base_url + +stages: + - name: Upload a protocol + request: + url: '{ot2_server_base_url}/protocols' + method: POST + files: + files: 'tests/integration/protocols/basic_transfer_with_run_time_parameters.py' + response: + status_code: 201 + save: + json: + protocol_id: data.id + + - name: Create run from protocol + request: + url: '{ot2_server_base_url}/runs' + method: POST + json: + data: + protocolId: '{protocol_id}' + runTimeParameterValues: + sample_count: 4 + volume: 10.23 + dry_run: True + pipette: flex_8channel_50 + response: + status_code: 201 + save: + json: + run_id: data.id + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: idle + current: True + actions: [] + errors: [] + pipettes: [] + modules: [] + labware: [] + labwareOffsets: [] + runTimeParameters: [] + liquids: [] + protocolId: '{protocol_id}' + + - name: Play the run + request: + url: '{ot2_server_base_url}/runs/{run_id}/actions' + method: POST + json: + data: + actionType: play + response: + status_code: 201 + json: + data: + id: !anystr + actionType: play + createdAt: !anystr + + - name: Wait for the protocol to complete + max_retries: 10 + delay_after: 0.1 + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + status: succeeded + + - name: Verify the run contains the set run time parameters + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: succeeded + current: True + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 4.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_8channel_50 + description: What pipette to use during the protocol. + protocolId: '{protocol_id}' + + - name: Mark the run as not-current + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: PATCH + json: + data: + current: False + response: + status_code: 200 + + - name: Verify the archived run still contains the set run time parameters + request: + url: '{ot2_server_base_url}/runs/{run_id}' + method: GET + response: + status_code: 200 + strict: + - json:off + json: + data: + id: !anystr + ok: True + createdAt: !anystr + status: succeeded + current: False + runTimeParameters: + - displayName: Sample count + variableName: sample_count + type: int + default: 6.0 + min: 1.0 + max: 12.0 + value: 4.0 + description: How many samples to process. + - displayName: Pipette volume + variableName: volume + type: float + default: 20.1 + choices: + - displayName: Low Volume + value: 10.23 + - displayName: Medium Volume + value: 20.1 + - displayName: High Volume + value: 50.5 + value: 10.23 + description: How many microliters to pipette of each sample. + - displayName: Dry Run + variableName: dry_run + type: bool + default: false + value: true + description: Skip aspirate and dispense steps. + - displayName: Pipette Name + variableName: pipette + type: str + choices: + - displayName: Single channel 50µL + value: flex_1channel_50 + - displayName: Eight Channel 50µL + value: flex_8channel_50 + default: flex_1channel_50 + value: flex_8channel_50 + description: What pipette to use during the protocol. + protocolId: '{protocol_id}' diff --git a/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py new file mode 100644 index 000000000000..7fe90c65d8cf --- /dev/null +++ b/robot-server/tests/integration/protocols/basic_transfer_with_run_time_parameters.py @@ -0,0 +1,57 @@ +from opentrons.protocol_api import ProtocolContext, ParameterContext + +metadata = { + "apiLevel": "2.18", + "author": "engineer@opentrons.com", + "protocolName": "basic_transfer_standalone", +} + + +def add_parameters(parameters: ParameterContext): + parameters.add_int( + display_name="Sample count", + variable_name="sample_count", + default=6, + minimum=1, + maximum=12, + description="How many samples to process.", + ) + parameters.add_float( + display_name="Pipette volume", + variable_name="volume", + default=20.1, + choices=[ + {"display_name": "Low Volume", "value": 10.23}, + {"display_name": "Medium Volume", "value": 20.1}, + {"display_name": "High Volume", "value": 50.5}, + ], + description="How many microliters to pipette of each sample.", + unit="µL", # Unit is not wired up, and it doesn't raise errors either. + ) + parameters.add_bool( + display_name="Dry Run", + variable_name="dry_run", + default=False, + description="Skip aspirate and dispense steps.", + ) + parameters.add_str( + display_name="Pipette Name", + variable_name="pipette", + choices=[ + {"display_name": "Single channel 50µL", "value": "flex_1channel_50"}, + {"display_name": "Eight Channel 50µL", "value": "flex_8channel_50"}, + ], + default="flex_1channel_50", + description="What pipette to use during the protocol.", + ) + + +def run(protocol: ProtocolContext) -> None: + plate = protocol.load_labware("corning_96_wellplate_360ul_flat", 1) + tiprack_1 = protocol.load_labware("opentrons_96_tiprack_300ul", 2) + p300 = protocol.load_instrument("p300_single", "right", tip_racks=[tiprack_1]) + + p300.pick_up_tip() + p300.aspirate(100, plate["A1"]) + p300.dispense(100, plate["B1"]) + p300.return_tip() diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py new file mode 100644 index 000000000000..227d65cd00b4 --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_papi.py @@ -0,0 +1,13 @@ +from opentrons.protocol_api import ProtocolContext + +metadata = { + "protocolName": "stop while waiting test", + "author": "Opentrons ", + "apiLevel": "2.15", +} + + +def run(ctx: ProtocolContext) -> None: + ctx.home() + ctx.delay(seconds=30) + ctx.set_rail_lights(on=True) diff --git a/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json new file mode 100644 index 000000000000..05101595ee7f --- /dev/null +++ b/robot-server/tests/integration/protocols/wait_for_resume_stop_v6.json @@ -0,0 +1,37 @@ +{ + "$otSharedSchema": "#/protocol/schemas/6", + "schemaVersion": 6, + "metadata": { + "protocolName": "Simple test protocol", + "author": "engineering ", + "description": "A short test protocol", + "created": 1223131231, + "tags": ["unitTest"] + }, + "robot": { + "model": "OT-2 Standard", + "deckId": "ot2_standard" + }, + "pipettes": {}, + "modules": {}, + "labware": {}, + "liquids": {}, + "labwareDefinitions": {}, + "commands": [ + { + "commandType": "home", + "params": {} + }, + { + "commandType": "waitForDuration", + "params": { + "seconds": 30 + } + }, + { + "commandType": "home", + "params": {} + } + ], + "commandAnnotations": [] +} diff --git a/robot-server/tests/integration/robot_client.py b/robot-server/tests/integration/robot_client.py index 90869cdde92c..c4511f8d3155 100644 --- a/robot-server/tests/integration/robot_client.py +++ b/robot-server/tests/integration/robot_client.py @@ -312,6 +312,18 @@ async def delete_session(self, session_id: str) -> Response: response.raise_for_status() return response + async def put_deck_configuration( + self, + req_body: Dict[str, object], + ) -> Response: + """PUT /deck_configuration.""" + response = await self.httpx_client.put( + url=f"{self.base_url}/deck_configuration", + json=req_body, + ) + response.raise_for_status() + return response + async def poll_until_run_completes( robot_client: RobotClient, run_id: str, poll_interval: float = _RUN_POLL_INTERVAL diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 4e2b8b399e54..2f61afcac48e 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -36,6 +36,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -79,6 +84,7 @@ async def test_create_run( created_at=run_created_at, labware_offsets=[labware_offset_create], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -91,6 +97,7 @@ async def test_create_run( created_at=run_created_at, is_ok_to_create_maintenance_run=True, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index d0a3ccfc1c8c..948705572ce0 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -6,6 +6,7 @@ from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.types import DeckSlotName from opentrons.hardware_control import API from opentrons.hardware_control.types import EstopStateNotification, EstopState @@ -20,12 +21,17 @@ MaintenanceEngineStore, EngineConflictError, NoRunnerEnginePairError, - get_estop_listener, + handle_estop_event, ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture -def subject(decoy: Decoy) -> MaintenanceEngineStore: +async def subject(decoy: Decoy) -> MaintenanceEngineStore: """Get a MaintenanceEngineStore test subject.""" # TODO(mc, 2021-06-11): to make these test more effective and valuable, we # should pass in some sort of actual, valid HardwareAPI instead of a mock @@ -42,7 +48,10 @@ def subject(decoy: Decoy) -> MaintenanceEngineStore: async def test_create_engine(subject: MaintenanceEngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 1, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -67,7 +76,10 @@ async def test_create_engine_uses_robot_and_deck_type( ) await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 4, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 4, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -88,6 +100,7 @@ async def test_create_engine_with_labware_offsets( run_id="run-id", labware_offsets=[labware_offset], created_at=datetime(2023, 1, 1), + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -104,7 +117,10 @@ async def test_create_engine_with_labware_offsets( async def test_clear_engine(subject: MaintenanceEngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 5, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 5, 1), + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -124,7 +140,10 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 6, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 6, 1), + notify_publishers=mock_notify_publishers, ) subject.runner.play() @@ -135,7 +154,10 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: MaintenanceEngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], created_at=datetime(2023, 7, 1) + run_id="run-id", + labware_offsets=[], + created_at=datetime(2023, 7, 1), + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None @@ -155,22 +177,30 @@ async def test_estop_callback( """The callback should stop an active engine.""" engine_store = decoy.mock(cls=MaintenanceEngineStore) - subject = get_estop_listener(engine_store=engine_store) - - decoy.when(engine_store.current_run_id).then_return(None, "fake_run_id") - disengage_event = EstopStateNotification( old_state=EstopState.PHYSICALLY_ENGAGED, new_state=EstopState.LOGICALLY_ENGAGED ) - - subject(disengage_event) - engage_event = EstopStateNotification( old_state=EstopState.LOGICALLY_ENGAGED, new_state=EstopState.PHYSICALLY_ENGAGED ) - subject(engage_event) - - subject(engage_event) + decoy.when(engine_store.current_run_id).then_return(None) + await handle_estop_event(engine_store, disengage_event) + decoy.verify( + engine_store.engine.estop(), + ignore_extra_args=True, + times=0, + ) + decoy.verify( + await engine_store.engine.finish(), + ignore_extra_args=True, + times=0, + ) - decoy.verify(engine_store.engine.estop(maintenance_run=True), times=1) + decoy.when(engine_store.current_run_id).then_return("fake-run-id") + await handle_estop_event(engine_store, engage_event) + decoy.verify( + engine_store.engine.estop(), + await engine_store.engine.finish(error=matchers.IsA(EStopActivatedError)), + times=1, + ) diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index f0e63809d683..0046b3098db2 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -35,6 +35,11 @@ from opentrons.protocol_engine import Liquid +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_maintenance_engine_store(decoy: Decoy) -> MaintenanceEngineStore: """Get a mock MaintenanceEngineStore.""" @@ -104,6 +109,7 @@ async def test_create( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -114,6 +120,7 @@ async def test_create( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -153,6 +160,7 @@ async def test_create_with_options( labware_offsets=[labware_offset], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -164,6 +172,7 @@ async def test_create_with_options( created_at=created_at, labware_offsets=[labware_offset], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert result == MaintenanceRun( @@ -196,6 +205,7 @@ async def test_create_engine_error( labware_offsets=[], created_at=created_at, deck_configuration=[], + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -208,6 +218,7 @@ async def test_create_engine_error( created_at=created_at, labware_offsets=[], deck_configuration=[], + notify_publishers=mock_notify_publishers, ) diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index ca0bca5c2d56..5f3c45adcaab 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -10,6 +10,7 @@ metadata as latest_metadata, schema_3, schema_2, + schema_4, ) # The statements that we expect to emit when we create a fresh database. @@ -39,6 +40,7 @@ protocol_id VARCHAR NOT NULL, analyzer_version VARCHAR NOT NULL, completed_analysis VARCHAR NOT NULL, + run_time_parameter_values_and_defaults VARCHAR, PRIMARY KEY (id), FOREIGN KEY(protocol_id) REFERENCES protocol (id) ) @@ -54,6 +56,7 @@ state_summary VARCHAR, engine_status VARCHAR, _updated_at DATETIME, + run_time_parameters VARCHAR, PRIMARY KEY (id), FOREIGN KEY(protocol_id) REFERENCES protocol (id) ) @@ -87,8 +90,70 @@ """, ] +EXPECTED_STATEMENTS_V4 = EXPECTED_STATEMENTS_LATEST -EXPECTED_STATEMENTS_V3 = EXPECTED_STATEMENTS_LATEST +EXPECTED_STATEMENTS_V3 = [ + """ + CREATE TABLE protocol ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_key VARCHAR, + PRIMARY KEY (id) + ) + """, + """ + CREATE TABLE analysis ( + id VARCHAR NOT NULL, + protocol_id VARCHAR NOT NULL, + analyzer_version VARCHAR NOT NULL, + completed_analysis VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE INDEX ix_analysis_protocol_id ON analysis (protocol_id) + """, + """ + CREATE TABLE run ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + protocol_id VARCHAR, + state_summary VARCHAR, + engine_status VARCHAR, + _updated_at DATETIME, + PRIMARY KEY (id), + FOREIGN KEY(protocol_id) REFERENCES protocol (id) + ) + """, + """ + CREATE TABLE action ( + id VARCHAR NOT NULL, + created_at DATETIME NOT NULL, + action_type VARCHAR NOT NULL, + run_id VARCHAR NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE TABLE run_command ( + row_id INTEGER NOT NULL, + run_id VARCHAR NOT NULL, + index_in_run INTEGER NOT NULL, + command_id VARCHAR NOT NULL, + command VARCHAR NOT NULL, + PRIMARY KEY (row_id), + FOREIGN KEY(run_id) REFERENCES run (id) + ) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_command_id ON run_command (run_id, command_id) + """, + """ + CREATE UNIQUE INDEX ix_run_run_id_index_in_run ON run_command (run_id, index_in_run) + """, +] EXPECTED_STATEMENTS_V2 = [ @@ -165,6 +230,7 @@ def _normalize_statement(statement: str) -> str: ("metadata", "expected_statements"), [ (latest_metadata, EXPECTED_STATEMENTS_LATEST), + (schema_4.metadata, EXPECTED_STATEMENTS_V4), (schema_3.metadata, EXPECTED_STATEMENTS_V3), (schema_2.metadata, EXPECTED_STATEMENTS_V2), ], @@ -172,7 +238,7 @@ def _normalize_statement(statement: str) -> str: def test_creating_tables_emits_expected_statements( metadata: sqlalchemy.MetaData, expected_statements: List[str] ) -> None: - """Test that fresh databases are created with with the expected statements. + """Test that fresh databases are created with the expected statements. This is a snapshot test to help catch accidental changes to our SQL schema. diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index b9c2dcccdac4..090cb680dfe0 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -6,6 +6,8 @@ from typing import List, NamedTuple import pytest +from decoy import Decoy +from opentrons.protocol_engine.types import RunTimeParamValuesType from sqlalchemy.engine import Engine as SQLEngine @@ -28,10 +30,17 @@ AnalysisSummary, PendingAnalysis, CompletedAnalysis, + RunTimeParameterAnalysisData, ) from robot_server.protocols.analysis_store import ( AnalysisStore, AnalysisNotFoundError, + AnalysisIsPendingError, + _CURRENT_ANALYZER_VERSION, +) +from robot_server.protocols.completed_analysis_store import ( + CompletedAnalysisStore, + CompletedAnalysisResource, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -171,12 +180,20 @@ async def test_update_adds_details_and_completes_analysis( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - + run_time_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", robot_type="OT-2 Standard", - run_time_parameters=[], + run_time_parameters=[run_time_param], labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -195,7 +212,7 @@ async def test_update_adds_details_and_completes_analysis( status=AnalysisStatus.COMPLETED, result=AnalysisResult.OK, robotType="OT-2 Standard", - runTimeParameters=[], + runTimeParameters=[run_time_param], labware=[labware], pipettes=[pipette], modules=[], @@ -209,7 +226,17 @@ async def test_update_adds_details_and_completes_analysis( "result": "ok", "status": "completed", "robotType": "OT-2 Standard", - "runTimeParameters": [], + "runTimeParameters": [ + { + "displayName": "My parameter", + "variableName": "cool_param", + "type": "int", + "min": 1, + "max": 5, + "value": 2.0, + "default": 3.0, + } + ], "labware": [ { "id": "labware-id", @@ -228,6 +255,76 @@ async def test_update_adds_details_and_completes_analysis( } +async def test_update_adds_rtp_values_and_defaults_to_completed_store( + decoy: Decoy, sql_engine: SQLEngine, protocol_store: ProtocolStore +) -> None: + """It should add RTP values and defaults to completed analysis store.""" + number_param = pe_types.NumberParameter( + displayName="My parameter", + variableName="cool_param", + type="int", + min=1, + max=5, + value=2.0, + default=3.0, + ) + string_param = pe_types.EnumParameter( + displayName="A choiced param", + variableName="cooler_param", + type="str", + choices=[ + pe_types.EnumChoice(displayName="FOOOO", value="foo"), + pe_types.EnumChoice(displayName="BARRR", value="bar"), + ], + value="baz", + default="blah", + ) + expected_completed_analysis_resource = CompletedAnalysisResource( + id="analysis-id", + protocol_id="protocol-id", + analyzer_version=_CURRENT_ANALYZER_VERSION, + completed_analysis=CompletedAnalysis( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + result=AnalysisResult.OK, + robotType="OT-2 Standard", + runTimeParameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ), + run_time_parameter_values_and_defaults={ + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData(value="baz", default="blah"), + }, + ) + + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") + await subject.update( + analysis_id="analysis-id", + robot_type="OT-2 Standard", + run_time_parameters=[number_param, string_param], + labware=[], + pipettes=[], + modules=[], + commands=[], + errors=[], + liquids=[], + ) + decoy.verify( + await mock_completed_store.make_room_and_add( + completed_analysis_resource=expected_completed_analysis_resource + ) + ) + + class AnalysisResultSpec(NamedTuple): """Spec data for analysis result tests.""" @@ -291,3 +388,101 @@ async def test_update_infers_status_from_errors( analysis = (await subject.get_by_protocol("protocol-id"))[0] assert isinstance(analysis, CompletedAnalysis) assert analysis.result == expected_result + + +@pytest.mark.parametrize( + argnames=["rtp_values_from_client", "expected_match"], + argvalues=[ + ({"cool_param": 2.0, "cooler_param": "baz", "uncool_param": 5}, True), + ( + {"cool_param": 2, "cooler_param": "baz"}, + True, + ), + ( + {"cool_param": 2, "cooler_param": "buzzzzzzz"}, + False, + ), + ( + {"cool_param": 2.0, "cooler_param": "baz", "weird_param": 5}, + False, + ), + ({}, False), + ], +) +async def test_matching_rtp_values_in_analysis( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, + rtp_values_from_client: RunTimeParamValuesType, + expected_match: bool, +) -> None: + """It should return whether the client's RTP values match with those in the last analysis of protocol.""" + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return( + { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=3.0), + "cooler_param": RunTimeParameterAnalysisData( + value="baz", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=5, default=5), + } + ) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values=rtp_values_from_client, + ) + == expected_match + ) + + +async def test_matching_default_rtp_values_in_analysis_with_no_client_rtp_values( + decoy: Decoy, + sql_engine: SQLEngine, + protocol_store: ProtocolStore, +) -> None: + """It should return a match when client sends no RTP values and last analysis used all default values.""" + params_with_only_default_values = { + "cool_param": RunTimeParameterAnalysisData(value=2.0, default=2.0), + "cooler_param": RunTimeParameterAnalysisData( + value="very cool", default="very cool" + ), + "uncool_param": RunTimeParameterAnalysisData(value=True, default=True), + } + mock_completed_store = decoy.mock(cls=CompletedAnalysisStore) + subject = AnalysisStore(sql_engine=sql_engine, completed_store=mock_completed_store) + protocol_store.insert(make_dummy_protocol_resource(protocol_id="protocol-id")) + + decoy.when( + await mock_completed_store.get_rtp_values_and_defaults_by_analysis_id( + "analysis-2" + ) + ).then_return(params_with_only_default_values) + assert ( + await subject.matching_rtp_values_in_analysis( + analysis_summary=AnalysisSummary( + id="analysis-2", status=AnalysisStatus.COMPLETED + ), + new_rtp_values={}, + ) + is True + ) + + +async def test_matching_default_rtp_values_in_analysis_with_pending_analysis( + subject: AnalysisStore, protocol_store: ProtocolStore +) -> None: + """It should raise an error if analysis is pending.""" + with pytest.raises(AnalysisIsPendingError): + await subject.matching_rtp_values_in_analysis( + AnalysisSummary(id="analysis-id", status=AnalysisStatus.PENDING), {} + ) diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 8339460cf66e..1cac25fb4e1e 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -2,11 +2,13 @@ import json from datetime import datetime, timezone from pathlib import Path +from typing import Optional, Dict, List import pytest from sqlalchemy.engine import Engine from decoy import Decoy +from robot_server.persistence.tables import analysis_table from robot_server.protocols.completed_analysis_store import ( CompletedAnalysisResource, CompletedAnalysisStore, @@ -20,6 +22,7 @@ CompletedAnalysis, AnalysisResult, AnalysisStatus, + RunTimeParameterAnalysisData, ) from robot_server.protocols.protocol_store import ( ProtocolStore, @@ -76,7 +79,9 @@ def make_dummy_protocol_resource(protocol_id: str) -> ProtocolResource: def _completed_analysis_resource( - analysis_id: str, protocol_id: str + analysis_id: str, + protocol_id: str, + rtp_values_and_defaults: Optional[Dict[str, RunTimeParameterAnalysisData]] = None, ) -> CompletedAnalysisResource: return CompletedAnalysisResource( analysis_id, @@ -93,6 +98,7 @@ def _completed_analysis_resource( errors=[], liquids=[], ), + run_time_parameter_values_and_defaults=rtp_values_and_defaults or {}, ) @@ -120,7 +126,7 @@ async def test_get_by_analysis_id_falls_back_to_sql( """It should return analyses from sql if they are not cached.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) # the analysis is not cached decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) analysis_from_sql = await subject.get_by_id("analysis-id") @@ -137,7 +143,7 @@ async def test_get_by_analysis_id_stores_results_in_cache( """It should cache successful fetches from sql.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) # the analysis is not cached decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) from_sql = await subject.get_by_id("analysis-id") @@ -152,7 +158,7 @@ async def test_get_by_analysis_id_as_document( """It should return the analysis serialized as a JSON string.""" resource = _completed_analysis_resource("analysis-id", "protocol-id") protocol_store.insert(make_dummy_protocol_resource("protocol-id")) - await subject.add(resource) + await subject.make_room_and_add(resource) result = await subject.get_by_id_as_document("analysis-id") assert result is not None assert json.loads(result) == { @@ -178,9 +184,9 @@ async def test_get_ids_by_protocol( resource_3 = _completed_analysis_resource("analysis-id-3", "protocol-id-2") protocol_store.insert(make_dummy_protocol_resource("protocol-id-1")) protocol_store.insert(make_dummy_protocol_resource("protocol-id-2")) - await subject.add(resource_1) - await subject.add(resource_2) - await subject.add(resource_3) + await subject.make_room_and_add(resource_1) + await subject.make_room_and_add(resource_2) + await subject.make_room_and_add(resource_3) assert subject.get_ids_by_protocol("protocol-id-1") == [ "analysis-id-1", "analysis-id-2", @@ -202,9 +208,9 @@ async def test_get_by_protocol( decoy.when(memcache.insert("analysis-id-1", resource_1)).then_return(None) decoy.when(memcache.insert("analysis-id-2", resource_2)).then_return(None) decoy.when(memcache.insert("analysis-id-3", resource_3)).then_return(None) - await subject.add(resource_1) - await subject.add(resource_2) - await subject.add(resource_3) + await subject.make_room_and_add(resource_1) + await subject.make_room_and_add(resource_2) + await subject.make_room_and_add(resource_3) decoy.when(memcache.get("analysis-id-1")).then_raise(KeyError()) decoy.when(memcache.get("analysis-id-2")).then_return(resource_2) decoy.when(memcache.contains("analysis-id-1")).then_return(False) @@ -212,3 +218,143 @@ async def test_get_by_protocol( decoy.when(memcache.insert("analysis-id-1", resource_1)).then_return(None) resources = await subject.get_by_protocol("protocol-id-1") assert resources == [resource_1, resource_2] + + +async def test_get_rtp_values_and_defaults_by_analysis_id_prefers_memcache( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should return RTP values and defaults dict from memcache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "abc": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + # When we retrieve a resource via its id we should see it query the cache, and it should + # return the identity-same resource + decoy.when(memcache.get("analysis-id")).then_return(resource) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults + + +async def test_get_rtp_values_and_defaults_by_analysis_from_db( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + decoy: Decoy, +) -> None: + """It should fetch the RTP values and defaults dict from database if not present in cache.""" + resource = _completed_analysis_resource( + analysis_id="analysis-id", + protocol_id="protocol-id", + rtp_values_and_defaults={ + "xyz": RunTimeParameterAnalysisData(value=123, default=234) + }, + ) + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + await subject.make_room_and_add(resource) + # Not in memcache + decoy.when(memcache.get("analysis-id")).then_raise(KeyError()) + result = await subject.get_rtp_values_and_defaults_by_analysis_id("analysis-id") + assert result == resource.run_time_parameter_values_and_defaults + + +@pytest.mark.parametrize( + argnames=["existing_analysis_ids", "expected_analyses_ids_after_making_room"], + argvalues=[ + ( + [f"analysis-id-{num}" for num in range(8)], + [ + "analysis-id-4", + "analysis-id-5", + "analysis-id-6", + "analysis-id-7", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(5)], + [ + "analysis-id-1", + "analysis-id-2", + "analysis-id-3", + "analysis-id-4", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(4)], + [ + "analysis-id-0", + "analysis-id-1", + "analysis-id-2", + "analysis-id-3", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(3)], + [ + "analysis-id-0", + "analysis-id-1", + "analysis-id-2", + "new-analysis-id", + ], + ), + ( + [f"analysis-id-{num}" for num in range(2)], + ["analysis-id-0", "analysis-id-1", "new-analysis-id"], + ), + (["analysis-id-0"], ["analysis-id-0", "new-analysis-id"]), + ([], ["new-analysis-id"]), + ], +) +async def test_add_makes_room_for_new_analysis( + subject: CompletedAnalysisStore, + memcache: MemoryCache[str, CompletedAnalysisResource], + protocol_store: ProtocolStore, + existing_analysis_ids: List[str], + expected_analyses_ids_after_making_room: List[str], + decoy: Decoy, + sql_engine: Engine, +) -> None: + """It should delete old analyses and make room for new analysis.""" + protocol_store.insert(make_dummy_protocol_resource("protocol-id")) + + # Set up the database with existing analyses + resources = [ + _completed_analysis_resource( + analysis_id=analysis_id, + protocol_id="protocol-id", + ) + for analysis_id in existing_analysis_ids + ] + for resource in resources: + statement = analysis_table.insert().values(await resource.to_sql_values()) + with sql_engine.begin() as transaction: + transaction.execute(statement) + + assert subject.get_ids_by_protocol("protocol-id") == existing_analysis_ids + await subject.make_room_and_add( + _completed_analysis_resource( + analysis_id="new-analysis-id", + protocol_id="protocol-id", + ) + ) + assert ( + subject.get_ids_by_protocol("protocol-id") + == expected_analyses_ids_after_making_room + ) + + removed_ids = [ + analysis_id + for analysis_id in existing_analysis_ids + if analysis_id not in expected_analyses_ids_after_making_room + ] + for analysis_id in removed_ids: + decoy.verify(memcache.remove(analysis_id)) diff --git a/robot-server/tests/protocols/test_memcache.py b/robot-server/tests/protocols/test_memcache.py index ce485d8984f7..80acb184f206 100644 --- a/robot-server/tests/protocols/test_memcache.py +++ b/robot-server/tests/protocols/test_memcache.py @@ -22,3 +22,19 @@ def test_cache_retains_new_values() -> None: for val in range(1, 4): assert subject.contains(f"key-{val}") assert subject.get(f"key-{val}") == f"value-{val}" + + +def test_cache_removes_values_by_key() -> None: + """It should eject values when asked for it.""" + subject = MemoryCache(3, str, str) + for val in range(3): + subject.insert(f"key-{val}", f"value-{val}") + subject.remove("key-1") + assert not subject.contains("key-1") + + # Make sure cache order is updated + assert subject.contains("key-0") and subject.contains("key-2") + subject.insert("key-4", "value-4") + assert subject.contains("key-0") + subject.insert("key-5", "value-5") + assert not subject.contains("key-0") diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index bd6655e4c108..d75212fd2fe3 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -50,7 +50,7 @@ def mock_runs_publisher(decoy: Decoy) -> RunsPublisher: @pytest.fixture def run_store(sql_engine: SQLEngine, mock_runs_publisher: RunsPublisher) -> RunStore: """Get a RunStore linked to the same database as the subject ProtocolStore.""" - return RunStore(sql_engine=sql_engine, runs_publisher=mock_runs_publisher) + return RunStore(sql_engine=sql_engine) async def test_insert_and_get_protocol( diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index dbdad50c3bd3..88605f81a3b3 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -1,11 +1,13 @@ """Tests for the /protocols router.""" import io + import pytest from datetime import datetime from decoy import Decoy, matchers from fastapi import UploadFile from pathlib import Path +from opentrons.protocol_engine.types import RunTimeParamValuesType from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_reader import ( @@ -22,9 +24,13 @@ ) from robot_server.errors.error_responses import ApiError -from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta +from robot_server.service.json_api import SimpleEmptyBody, MultiBodyMeta, RequestModel from robot_server.service.task_runner import TaskRunner -from robot_server.protocols.analysis_store import AnalysisStore, AnalysisNotFoundError +from robot_server.protocols.analysis_store import ( + AnalysisStore, + AnalysisNotFoundError, + AnalysisIsPendingError, +) from robot_server.protocols.protocol_analyzer import ProtocolAnalyzer from robot_server.protocols.protocol_auto_deleter import ProtocolAutoDeleter from robot_server.protocols.analysis_models import ( @@ -33,6 +39,7 @@ CompletedAnalysis, PendingAnalysis, AnalysisResult, + AnalysisRequest, ) from robot_server.protocols.protocol_models import ( @@ -51,6 +58,7 @@ from robot_server.protocols.router import ( ProtocolLinks, create_protocol, + create_protocol_analysis, get_protocols, get_protocol_ids, get_protocol_by_id, @@ -373,6 +381,11 @@ async def test_create_existing_protocol( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return([completed_analysis]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=completed_analysis, new_rtp_values={} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -513,12 +526,12 @@ async def test_create_protocol( protocol_analyzer.analyze, analysis_id="analysis-id", protocol_resource=protocol_resource, - run_time_param_values=None, + run_time_param_values={}, ), ) -async def test_create_protocol_with_run_time_params( +async def test_create_new_protocol_with_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -620,7 +633,240 @@ async def test_create_protocol_with_run_time_params( ) -async def test_create_existing_protocol_with_run_time_params( +async def test_create_existing_protocol_with_no_previous_analysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + pending_analysis = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([]) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_analysis) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[pending_analysis], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_different_run_time_params( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should re-trigger analysis of the existing protocol resource.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", + ) + + completed_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ) + + pending_summary = AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ) + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return([completed_summary]) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + completed_summary, {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(False) + decoy.when( + analysis_store.add_pending( + protocol_id="the-og-proto-id", analysis_id="analysis-id" + ) + ).then_return(pending_summary) + + result = await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert result.content.data == Protocol( + id="the-og-proto-id", + createdAt=datetime(year=2020, month=1, day=1), + protocolType=ProtocolType.JSON, + metadata=Metadata(this_is_fake_metadata=True), # type: ignore[call-arg] + robotType="OT-2 Standard", + analysisSummaries=[completed_summary, pending_summary], + files=[ProtocolFile(name="foo.json", role=ProtocolFileRole.MAIN)], + key="dummy-key-222", + ) + assert result.status_code == 200 + decoy.verify( + task_runner.run( + protocol_analyzer.analyze, + analysis_id="analysis-id", + protocol_resource=stored_protocol_resource, + run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, + ), + analysis_store.add_pending( + protocol_id="the-og-proto-id", + analysis_id="analysis-id", + ), + ) + + +async def test_create_existing_protocol_with_same_run_time_params( decoy: Decoy, protocol_store: ProtocolStore, analysis_store: AnalysisStore, @@ -666,10 +912,6 @@ async def test_create_existing_protocol_with_run_time_params( id="analysis-id", status=AnalysisStatus.COMPLETED, ), - AnalysisSummary( - id="analysis-id", - status=AnalysisStatus.PENDING, - ), ] decoy.when( @@ -689,6 +931,11 @@ async def test_create_existing_protocol_with_run_time_params( decoy.when( analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_return(True) result = await create_protocol( files=[protocol_file], @@ -727,12 +974,111 @@ async def test_create_existing_protocol_with_run_time_params( protocol_resource=stored_protocol_resource, run_time_param_values={"vol": 123, "dry_run": True, "mount": "left"}, ), + times=0, + ) + decoy.verify( analysis_store.add_pending( protocol_id="the-og-proto-id", analysis_id="analysis-id", ), + times=0, + ) + + +async def test_create_existing_protocol_with_pending_analysis_raises( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_reader: ProtocolReader, + file_reader_writer: FileReaderWriter, + file_hasher: FileHasher, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, + protocol_auto_deleter: ProtocolAutoDeleter, +) -> None: + """It should raise an error if protocol has existing pending analysis.""" + protocol_directory = Path("/dev/null") + content = bytes("some_content", encoding="utf-8") + uploaded_file = io.BytesIO(content) + + protocol_file = UploadFile(filename="foo.json", file=uploaded_file) + buffered_file = BufferedFile(name="blah", contents=content, path=None) + + protocol_source = ProtocolSource( + directory=Path("/dev/null"), + main_file=Path("/dev/null/foo.json"), + files=[ + ProtocolSourceFile( + path=Path("/dev/null/foo.json"), + role=ProtocolFileRole.MAIN, + ) + ], + metadata={"this_is_fake_metadata": True}, + robot_type="OT-2 Standard", + config=JsonProtocolConfig(schema_version=123), + content_hash="a_b_c", + ) + + stored_protocol_resource = ProtocolResource( + protocol_id="protocol-id", + created_at=datetime(year=2020, month=1, day=1), + source=protocol_source, + protocol_key="dummy-key-222", ) + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.PENDING, + ), + ] + + decoy.when( + await file_reader_writer.read( + # TODO(mm, 2024-02-07): Recent FastAPI upgrades mean protocol_file.filename + # is typed as possibly None. Investigate whether that can actually happen in + # practice and whether we need to account for it. + files=[protocol_file] # type: ignore[list-item] + ) + ).then_return([buffered_file]) + + decoy.when(await file_hasher.hash(files=[buffered_file])).then_return("a_b_c") + decoy.when(protocol_store.get_id_by_hash("a_b_c")).then_return("the-og-proto-id") + decoy.when(protocol_store.get(protocol_id="the-og-proto-id")).then_return( + stored_protocol_resource + ) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="the-og-proto-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], {"vol": 123, "dry_run": True, "mount": "left"} + ) + ).then_raise(AnalysisIsPendingError("a-id")) + + with pytest.raises(ApiError) as exc_info: + await create_protocol( + files=[protocol_file], + key="dummy-key-111", + run_time_parameter_values='{"vol": 123, "dry_run": true, "mount": "left"}', + protocol_directory=protocol_directory, + protocol_store=protocol_store, + analysis_store=analysis_store, + file_reader_writer=file_reader_writer, + protocol_reader=protocol_reader, + file_hasher=file_hasher, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + protocol_auto_deleter=protocol_auto_deleter, + robot_type="OT-2 Standard", + protocol_id="protocol-id", + analysis_id="analysis-id", + created_at=datetime(year=2021, month=1, day=1), + ) + + assert exc_info.value.status_code == 503 + assert exc_info.value.content["errors"][0]["id"] == "LastAnalysisPending" + async def test_create_protocol_not_readable( decoy: Decoy, @@ -1050,3 +1396,130 @@ async def test_get_protocol_analysis_as_document_analysis_not_found( assert exc_info.value.status_code == 404 assert exc_info.value.content["errors"][0]["id"] == "AnalysisNotFound" + + +async def test_create_protocol_analyses_with_same_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should not start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(True) + + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == analysis_summaries + assert result.status_code == 200 + + +async def test_update_protocol_analyses_with_new_rtp_values( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the new rtp values.""" + rtp_values: RunTimeParamValuesType = {"vol": 123, "dry_run": True, "mount": "left"} + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summaries[-1], rtp_values + ) + ).then_return(False) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel( + data=AnalysisRequest(runTimeParameterValues=rtp_values) + ), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 + + +async def test_update_protocol_analyses_with_forced_reanalysis( + decoy: Decoy, + protocol_store: ProtocolStore, + analysis_store: AnalysisStore, + protocol_analyzer: ProtocolAnalyzer, + task_runner: TaskRunner, +) -> None: + """It should start a new analysis for the protocol, regardless of rtp values.""" + analysis_summaries = [ + AnalysisSummary( + id="analysis-id", + status=AnalysisStatus.COMPLETED, + ), + ] + decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) + decoy.when( + analysis_store.get_summaries_by_protocol(protocol_id="protocol-id") + ).then_return(analysis_summaries) + decoy.when( + await analysis_store.matching_rtp_values_in_analysis( + analysis_summary=analysis_summaries[-1], new_rtp_values={} + ) + ).then_return(True) + decoy.when(analysis_store.add_pending("protocol-id", "analysis-id-2")).then_return( + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING) + ) + result = await create_protocol_analysis( + protocolId="protocol-id", + request_body=RequestModel(data=AnalysisRequest(forceReAnalyze=True)), + protocol_store=protocol_store, + analysis_store=analysis_store, + protocol_analyzer=protocol_analyzer, + task_runner=task_runner, + analysis_id="analysis-id-2", + ) + assert result.content.data == [ + AnalysisSummary(id="analysis-id", status=AnalysisStatus.COMPLETED), + AnalysisSummary(id="analysis-id-2", status=AnalysisStatus.PENDING), + ] + assert result.status_code == 201 diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 1fd754f224ad..5763935cc390 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -42,6 +42,11 @@ from robot_server.deck_configuration.store import DeckConfigurationStore +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: """Get a labware offset create request value object.""" @@ -87,6 +92,8 @@ async def test_create_run( labware_offsets=[labware_offset_create], deck_configuration=[], protocol=None, + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) @@ -99,6 +106,7 @@ async def test_create_run( created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -162,17 +170,24 @@ async def test_create_protocol_run( labware_offsets=[], deck_configuration=[], protocol=protocol_resource, + run_time_param_values={"foo": "bar"}, + notify_publishers=mock_notify_publishers, ) ).then_return(expected_response) result = await create_run( - request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), + request_body=RequestModel( + data=RunCreate( + protocolId="protocol-id", runTimeParameterValues={"foo": "bar"} + ) + ), protocol_store=mock_protocol_store, run_data_manager=mock_run_data_manager, run_id=run_id, created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert result.content.data == expected_response @@ -223,6 +238,8 @@ async def test_create_run_conflict( labware_offsets=[], deck_configuration=[], protocol=None, + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -234,6 +251,7 @@ async def test_create_run_conflict( run_data_manager=mock_run_data_manager, run_auto_deleter=mock_run_auto_deleter, deck_configuration_store=mock_deck_configuration_store, + notify_publishers=mock_notify_publishers, ) assert exc_info.value.status_code == 409 diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index 1bf746321397..330e974be9cb 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -1,12 +1,12 @@ """Tests for the EngineStore interface.""" from datetime import datetime -from pathlib import Path import pytest from decoy import Decoy, matchers from opentrons_shared_data import get_shared_data_root from opentrons_shared_data.robot.dev_types import RobotType +from opentrons.protocol_engine.errors.exceptions import EStopActivatedError from opentrons.types import DeckSlotName from opentrons.hardware_control import HardwareControlAPI, API from opentrons.hardware_control.types import EstopStateNotification, EstopState @@ -23,12 +23,17 @@ EngineStore, EngineConflictError, NoRunnerEnginePairError, - get_estop_listener, + handle_estop_event, ) +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture -def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: +async def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: """Get a EngineStore test subject.""" return EngineStore( hardware_api=hardware_api, @@ -40,7 +45,7 @@ def subject(decoy: Decoy, hardware_api: HardwareControlAPI) -> EngineStore: @pytest.fixture -async def json_protocol_source(tmp_path: Path) -> ProtocolSource: +async def json_protocol_source() -> ProtocolSource: """Get a protocol source fixture.""" simple_protocol = ( get_shared_data_root() / "protocol" / "fixtures" / "6" / "simpleV6.json" @@ -51,7 +56,11 @@ async def json_protocol_source(tmp_path: Path) -> ProtocolSource: async def test_create_engine(subject: EngineStore) -> None: """It should create an engine for a run.""" result = await subject.create( - run_id="run-id", labware_offsets=[], protocol=None, deck_configuration=[] + run_id="run-id", + labware_offsets=[], + protocol=None, + deck_configuration=[], + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" @@ -61,7 +70,6 @@ async def test_create_engine(subject: EngineStore) -> None: async def test_create_engine_with_protocol( - decoy: Decoy, subject: EngineStore, json_protocol_source: ProtocolSource, ) -> None: @@ -82,6 +90,7 @@ async def test_create_engine_with_protocol( labware_offsets=[], deck_configuration=[], protocol=protocol, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id" assert isinstance(result, StateSummary) @@ -103,7 +112,11 @@ async def test_create_engine_uses_robot_type( ) await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine.state_view.config.robot_type == robot_type @@ -122,6 +135,7 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: labware_offsets=[labware_offset], deck_configuration=[], protocol=None, + notify_publishers=mock_notify_publishers, ) assert result.labwareOffsets == [ @@ -138,12 +152,20 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> None: """It should not create more than one engine / runner pair.""" await subject.create( - run_id="run-id-1", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-1", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) with pytest.raises(EngineConflictError): await subject.create( - run_id="run-id-2", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id-2", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.current_run_id == "run-id-1" @@ -152,7 +174,11 @@ async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> async def test_clear_engine(subject: EngineStore) -> None: """It should clear a stored engine entry.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.runner.run(deck_configuration=[]) result = await subject.clear() @@ -172,7 +198,11 @@ async def test_clear_engine_not_stopped_or_idle( ) -> None: """It should raise a conflict if the engine is not stopped.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.runner.play(deck_configuration=[]) @@ -183,7 +213,11 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: EngineStore) -> None: """It should successfully clear engine if idle (not started).""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) assert subject.engine is not None assert subject.runner is not None @@ -216,7 +250,9 @@ async def test_get_default_engine_robot_type( # should pass in some sort of actual, valid HardwareAPI instead of a mock hardware_api = decoy.mock(cls=API) subject = EngineStore( - hardware_api=hardware_api, robot_type=robot_type, deck_type=deck_type + hardware_api=hardware_api, + robot_type=robot_type, + deck_type=deck_type, ) result = await subject.get_default_engine() @@ -227,7 +263,11 @@ async def test_get_default_engine_robot_type( async def test_get_default_engine_current_unstarted(subject: EngineStore) -> None: """It should allow a default engine if another engine current but unstarted.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) result = await subject.get_default_engine() @@ -237,7 +277,11 @@ async def test_get_default_engine_current_unstarted(subject: EngineStore) -> Non async def test_get_default_engine_conflict(subject: EngineStore) -> None: """It should not allow a default engine if another engine is executing commands.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) subject.engine.play() @@ -248,7 +292,11 @@ async def test_get_default_engine_conflict(subject: EngineStore) -> None: async def test_get_default_engine_run_stopped(subject: EngineStore) -> None: """It allow a default engine if another engine is terminal.""" await subject.create( - run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + run_id="run-id", + labware_offsets=[], + deck_configuration=[], + protocol=None, + notify_publishers=mock_notify_publishers, ) await subject.engine.finish() @@ -262,22 +310,30 @@ async def test_estop_callback( """The callback should stop an active engine.""" engine_store = decoy.mock(cls=EngineStore) - subject = get_estop_listener(engine_store=engine_store) - - decoy.when(engine_store.current_run_id).then_return(None, "fake_run_id") - disengage_event = EstopStateNotification( old_state=EstopState.PHYSICALLY_ENGAGED, new_state=EstopState.LOGICALLY_ENGAGED ) - - subject(disengage_event) - engage_event = EstopStateNotification( old_state=EstopState.LOGICALLY_ENGAGED, new_state=EstopState.PHYSICALLY_ENGAGED ) - subject(engage_event) - - subject(engage_event) + decoy.when(engine_store.current_run_id).then_return(None) + await handle_estop_event(engine_store, disengage_event) + decoy.verify( + engine_store.engine.estop(), + ignore_extra_args=True, + times=0, + ) + decoy.verify( + await engine_store.engine.finish(), + ignore_extra_args=True, + times=0, + ) - decoy.verify(engine_store.engine.estop(maintenance_run=False), times=1) + decoy.when(engine_store.current_run_id).then_return("fake-run-id") + await handle_estop_event(engine_store, engage_event) + decoy.verify( + engine_store.engine.estop(), + await engine_store.engine.finish(error=matchers.IsA(EStopActivatedError)), + times=1, + ) diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index 5bf5778c4864..a844cdcc6d57 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -11,6 +11,7 @@ commands as pe_commands, errors as pe_errors, ) +from opentrons.protocol_engine.types import RunTimeParameter, BooleanParameter from opentrons.protocol_runner import RunResult, JsonRunner, PythonAndLegacyRunner from robot_server.service.task_runner import TaskRunner @@ -60,6 +61,19 @@ def engine_state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + BooleanParameter( + displayName="Display Name", + variableName="variable_name", + value=False, + default=True, + ) + ] + + @pytest.fixture def protocol_commands() -> List[pe_commands.Command]: """Get a StateSummary value object.""" @@ -122,6 +136,7 @@ async def test_create_play_action_to_start( mock_run_store: RunStore, mock_task_runner: TaskRunner, engine_state_summary: StateSummary, + run_time_parameters: List[RunTimeParameter], protocol_commands: List[pe_commands.Command], run_id: str, subject: RunController, @@ -153,7 +168,7 @@ async def test_create_play_action_to_start( RunResult( commands=protocol_commands, state_summary=engine_state_summary, - parameters=[], + parameters=run_time_parameters, ) ) @@ -164,6 +179,7 @@ async def test_create_play_action_to_start( run_id=run_id, summary=engine_state_summary, commands=protocol_commands, + run_time_parameters=run_time_parameters, ), times=1, ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 92152eb39404..547ec0a7b747 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -1,5 +1,5 @@ """Tests for RunDataManager.""" -from typing import Optional +from typing import Optional, List import pytest from datetime import datetime @@ -40,6 +40,11 @@ from opentrons_shared_data.errors.exceptions import InvalidStoredData +def mock_notify_publishers() -> None: + """A mock notify_publishers.""" + return None + + @pytest.fixture def mock_engine_store(decoy: Decoy) -> EngineStore: """Get a mock EngineStore.""" @@ -80,6 +85,19 @@ def engine_state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[pe_types.RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + pe_types.BooleanParameter( + displayName="Display Name", + variableName="variable_name", + value=False, + default=True, + ) + ] + + @pytest.fixture def run_resource() -> RunResource: """Get a StateSummary value object.""" @@ -138,6 +156,8 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) decoy.when( @@ -154,6 +174,8 @@ async def test_create( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -180,7 +202,7 @@ async def test_create_with_options( engine_state_summary: StateSummary, run_resource: RunResource, ) -> None: - """It should handle creation with a protocol and labware offsets.""" + """It should handle creation with a protocol, labware offsets and parameters.""" run_id = "hello world" created_at = datetime(year=2021, month=1, day=1) @@ -203,6 +225,8 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -220,6 +244,8 @@ async def test_create_with_options( labware_offsets=[labware_offset], protocol=protocol, deck_configuration=[], + run_time_param_values={"foo": "bar"}, + notify_publishers=mock_notify_publishers, ) assert result == Run( @@ -254,6 +280,8 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) ).then_raise(EngineConflictError("oh no")) @@ -264,6 +292,8 @@ async def test_create_engine_error( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) decoy.verify( @@ -282,6 +312,7 @@ async def test_get_current_run( mock_run_store: RunStore, subject: RunDataManager, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, ) -> None: """It should get the current run from the engine.""" @@ -292,6 +323,9 @@ async def test_get_current_run( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( engine_state_summary ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + run_time_parameters + ) result = subject.get(run_id=run_id) @@ -308,6 +342,7 @@ async def test_get_current_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) assert subject.current_run_id == run_id @@ -318,6 +353,7 @@ async def test_get_historical_run( mock_run_store: RunStore, subject: RunDataManager, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, ) -> None: """It should get a historical run from the store.""" @@ -327,6 +363,9 @@ async def test_get_historical_run( decoy.when(mock_run_store.get_state_summary(run_id=run_id)).then_return( engine_state_summary ) + decoy.when(mock_run_store.get_run_time_parameters(run_id=run_id)).then_return( + run_time_parameters + ) decoy.when(mock_engine_store.current_run_id).then_return("some other id") result = subject.get(run_id=run_id) @@ -344,6 +383,7 @@ async def test_get_historical_run( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -353,6 +393,7 @@ async def test_get_historical_run_no_data( mock_run_store: RunStore, subject: RunDataManager, run_resource: RunResource, + run_time_parameters: List[pe_types.RunTimeParameter], ) -> None: """It should get a historical run from the store.""" run_id = "hello world" @@ -363,6 +404,9 @@ async def test_get_historical_run_no_data( decoy.when(mock_run_store.get_state_summary(run_id=run_id)).then_return( BadStateSummary(dataError=state_exc) ) + decoy.when(mock_run_store.get_run_time_parameters(run_id=run_id)).then_return( + run_time_parameters + ) decoy.when(mock_engine_store.current_run_id).then_return("some other id") result = subject.get(run_id=run_id) @@ -381,6 +425,7 @@ async def test_get_historical_run_no_data( pipettes=[], modules=[], liquids=[], + runTimeParameters=run_time_parameters, ) @@ -400,6 +445,14 @@ async def test_get_all_runs( modules=[LoadedModule.construct(id="current-module-id")], # type: ignore[call-arg] liquids=[Liquid(id="some-liquid-id", displayName="liquid", description="desc")], ) + current_run_time_parameters: List[pe_types.RunTimeParameter] = [ + pe_types.BooleanParameter( + displayName="Current Bool", + variableName="current bool", + value=False, + default=True, + ) + ] historical_run_data = StateSummary( status=EngineStatus.STOPPED, @@ -410,6 +463,14 @@ async def test_get_all_runs( modules=[LoadedModule.construct(id="old-module-id")], # type: ignore[call-arg] liquids=[], ) + historical_run_time_parameters: List[pe_types.RunTimeParameter] = [ + pe_types.BooleanParameter( + displayName="Old Bool", + variableName="Old bool", + value=True, + default=False, + ) + ] current_run_resource = RunResource( ok=True, @@ -431,9 +492,15 @@ async def test_get_all_runs( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( current_run_data ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + current_run_time_parameters + ) decoy.when(mock_run_store.get_state_summary("historical-run")).then_return( historical_run_data ) + decoy.when(mock_run_store.get_run_time_parameters("historical-run")).then_return( + historical_run_time_parameters + ) decoy.when(mock_run_store.get_all(length=20)).then_return( [historical_run_resource, current_run_resource] ) @@ -454,6 +521,7 @@ async def test_get_all_runs( pipettes=historical_run_data.pipettes, modules=historical_run_data.modules, liquids=historical_run_data.liquids, + runTimeParameters=historical_run_time_parameters, ), Run( current=True, @@ -468,6 +536,7 @@ async def test_get_all_runs( pipettes=current_run_data.pipettes, modules=current_run_data.modules, liquids=current_run_data.liquids, + runTimeParameters=current_run_time_parameters, ), ] @@ -509,6 +578,7 @@ async def test_delete_historical_run( async def test_update_current( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -520,7 +590,9 @@ async def test_update_current( decoy.when(mock_engine_store.current_run_id).then_return(run_id) decoy.when(await mock_engine_store.clear()).then_return( RunResult( - commands=[run_command], state_summary=engine_state_summary, parameters=[] + commands=[run_command], + state_summary=engine_state_summary, + parameters=run_time_parameters, ) ) @@ -529,6 +601,7 @@ async def test_update_current( run_id=run_id, summary=engine_state_summary, commands=[run_command], + run_time_parameters=run_time_parameters, ) ).then_return(run_resource) @@ -547,6 +620,7 @@ async def test_update_current( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -554,6 +628,7 @@ async def test_update_current( async def test_update_current_noop( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -567,6 +642,9 @@ async def test_update_current_noop( decoy.when(mock_engine_store.engine.state_view.get_summary()).then_return( engine_state_summary ) + decoy.when(mock_engine_store.runner.run_time_parameters).then_return( + run_time_parameters + ) decoy.when(mock_run_store.get(run_id=run_id)).then_return(run_resource) result = await subject.update(run_id=run_id, current=current) @@ -577,6 +655,7 @@ async def test_update_current_noop( run_id=run_id, summary=matchers.Anything(), commands=matchers.Anything(), + run_time_parameters=matchers.Anything(), ), times=0, ) @@ -594,6 +673,7 @@ async def test_update_current_noop( pipettes=engine_state_summary.pipettes, modules=engine_state_summary.modules, liquids=engine_state_summary.liquids, + runTimeParameters=run_time_parameters, ) @@ -617,6 +697,7 @@ async def test_update_current_not_allowed( async def test_create_archives_existing( decoy: Decoy, engine_state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], run_resource: RunResource, run_command: commands.Command, mock_engine_store: EngineStore, @@ -630,7 +711,9 @@ async def test_create_archives_existing( decoy.when(mock_engine_store.current_run_id).then_return(run_id_old) decoy.when(await mock_engine_store.clear()).then_return( RunResult( - commands=[run_command], state_summary=engine_state_summary, parameters=[] + commands=[run_command], + state_summary=engine_state_summary, + parameters=run_time_parameters, ) ) @@ -640,6 +723,8 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) ).then_return(engine_state_summary) @@ -657,6 +742,8 @@ async def test_create_archives_existing( labware_offsets=[], protocol=None, deck_configuration=[], + run_time_param_values=None, + notify_publishers=mock_notify_publishers, ) decoy.verify( @@ -664,6 +751,7 @@ async def test_create_archives_existing( run_id=run_id_old, summary=engine_state_summary, commands=[run_command], + run_time_parameters=run_time_parameters, ) ) diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index bb089d4b40a6..c6108cf54076 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -47,7 +47,6 @@ def subject( """Get a ProtocolStore test subject.""" return RunStore( sql_engine=sql_engine, - runs_publisher=mock_runs_publisher, ) @@ -121,6 +120,41 @@ def state_summary() -> StateSummary: ) +@pytest.fixture() +def run_time_parameters() -> List[pe_types.RunTimeParameter]: + """Get a RunTimeParameter list.""" + return [ + pe_types.BooleanParameter( + displayName="Display Name 1", + variableName="variable_name_1", + value=False, + default=True, + ), + pe_types.NumberParameter( + displayName="Display Name 2", + variableName="variable_name_2", + type="int", + min=123.0, + max=456.0, + value=333.0, + default=222.0, + ), + pe_types.EnumParameter( + displayName="Display Name 3", + variableName="variable_name_3", + type="str", + choices=[ + pe_types.EnumChoice( + displayName="Choice Name", + value="cool choice", + ) + ], + default="cooler choice", + value="coolest choice", + ), + ] + + @pytest.fixture def invalid_state_summary() -> StateSummary: """Should fail pydantic validation.""" @@ -165,6 +199,7 @@ def test_update_run_state( subject: RunStore, state_summary: StateSummary, protocol_commands: List[pe_commands.Command], + run_time_parameters: List[pe_types.RunTimeParameter], mock_runs_publisher: mock.Mock, ) -> None: """It should be able to update a run state to the store.""" @@ -185,8 +220,10 @@ def test_update_run_state( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=run_time_parameters, ) run_summary_result = subject.get_state_summary(run_id="run-id") + parameters_result = subject.get_run_time_parameters(run_id="run-id") commands_result = subject.get_commands_slice( run_id="run-id", length=len(protocol_commands), @@ -201,6 +238,7 @@ def test_update_run_state( actions=[action], ) assert run_summary_result == state_summary + assert parameters_result == run_time_parameters assert commands_result.commands == protocol_commands mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( run_id="run-id" @@ -218,6 +256,7 @@ def test_update_state_run_not_found( run_id="run-not-found", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) @@ -437,7 +476,9 @@ def test_get_state_summary( protocol_id=None, created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) - subject.update_run_state(run_id="run-id", summary=state_summary, commands=[]) + subject.update_run_state( + run_id="run-id", summary=state_summary, commands=[], run_time_parameters=[] + ) result = subject.get_state_summary(run_id="run-id") assert result == state_summary mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( @@ -455,7 +496,10 @@ def test_get_state_summary_failure( created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) subject.update_run_state( - run_id="run-id", summary=invalid_state_summary, commands=[] + run_id="run-id", + summary=invalid_state_summary, + commands=[], + run_time_parameters=[], ) result = subject.get_state_summary(run_id="run-id") assert isinstance(result, BadStateSummary) @@ -474,6 +518,62 @@ def test_get_state_summary_none(subject: RunStore) -> None: assert result.dataError.code == ErrorCodes.INVALID_STORED_DATA +def test_get_run_time_parameters( + subject: RunStore, + state_summary: StateSummary, + run_time_parameters: List[pe_types.RunTimeParameter], +) -> None: + """It should be able to get store run time parameters.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=[], + run_time_parameters=run_time_parameters, + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == run_time_parameters + + +def test_get_run_time_parameters_invalid( + subject: RunStore, + state_summary: StateSummary, +) -> None: + """It should return an empty list if there invalid parameters.""" + bad_parameters = [pe_types.BooleanParameter.construct(foo="bar")] # type: ignore[call-arg] + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", + summary=state_summary, + commands=[], + run_time_parameters=bad_parameters, # type: ignore[arg-type] + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == [] + + +def test_get_run_time_parameters_none( + subject: RunStore, + state_summary: StateSummary, +) -> None: + """It should return an empty list if there are no run time parameters associated.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + result = subject.get_run_time_parameters(run_id="run-id") + assert result == [] + + def test_has_run_id(subject: RunStore) -> None: """It should tell us if a given ID is in the store.""" subject.insert( @@ -504,6 +604,7 @@ def test_get_command( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_command(run_id="run-id", command_id="pause-2") @@ -533,6 +634,7 @@ def test_get_command_raise_exception( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) with pytest.raises(expected_exception): subject.get_command(run_id=input_run_id, command_id=input_command_id) @@ -553,6 +655,7 @@ def test_get_command_slice( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_commands_slice( run_id="run-id", cursor=0, length=len(protocol_commands) @@ -599,6 +702,7 @@ def test_get_commands_slice_clamping( run_id="run-id", summary=state_summary, commands=protocol_commands, + run_time_parameters=[], ) result = subject.get_commands_slice( run_id="run-id", cursor=input_cursor, length=input_length diff --git a/robot-server/tests/service/json_api/test_response.py b/robot-server/tests/service/json_api/test_response.py index 1429d88b5e0a..6952468229bb 100644 --- a/robot-server/tests/service/json_api/test_response.py +++ b/robot-server/tests/service/json_api/test_response.py @@ -116,7 +116,7 @@ class ResponseSpec(NamedTuple): "links": {"sibling": {"href": "/bar", "meta": None}}, }, ), - ResponseSpec(subject=NotifyRefetchBody(), expected={"refetchUsingHTTP": True}), + ResponseSpec(subject=NotifyRefetchBody(), expected={"refetch": True}), ResponseSpec( subject=NotifyUnsubscribeBody(), expected={"unsubscribe": True}, diff --git a/robot-server/tests/service/notifications/__init__.py b/robot-server/tests/service/notifications/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/robot-server/tests/service/notifications/publishers/__init__.py b/robot-server/tests/service/notifications/publishers/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py new file mode 100644 index 000000000000..8a0cb6a18329 --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_maintenance_runs_publisher.py @@ -0,0 +1,30 @@ +"""Tests for the maintenance runs publisher.""" +import pytest +from unittest.mock import AsyncMock + +from robot_server.service.notifications import MaintenanceRunsPublisher, Topics + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def maintenance_runs_publisher( + notification_client: AsyncMock, +) -> MaintenanceRunsPublisher: + """Instantiate MaintenanceRunsPublisher.""" + return MaintenanceRunsPublisher(notification_client) + + +@pytest.mark.asyncio +async def test_publish_current_maintenance_run( + notification_client: AsyncMock, maintenance_runs_publisher: MaintenanceRunsPublisher +) -> None: + """It should publish a notify flag for maintenance runs.""" + await maintenance_runs_publisher.publish_current_maintenance_run() + notification_client.publish_advise_refetch_async.assert_awaited_once_with( + topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN + ) diff --git a/robot-server/tests/service/notifications/publishers/test_runs_publisher.py b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py new file mode 100644 index 000000000000..29797dbf83a8 --- /dev/null +++ b/robot-server/tests/service/notifications/publishers/test_runs_publisher.py @@ -0,0 +1,145 @@ +"""Tests for runs publisher.""" +import pytest +from datetime import datetime +from unittest.mock import MagicMock, AsyncMock + +from robot_server.service.notifications import RunsPublisher, Topics +from opentrons.protocol_engine import CurrentCommand, EngineStatus + + +def mock_curent_command(command_id: str) -> CurrentCommand: + """Create a mock CurrentCommand.""" + return CurrentCommand( + command_id=command_id, + command_key="1", + index=0, + created_at=datetime(year=2021, month=1, day=1), + ) + + +@pytest.fixture +def notification_client() -> AsyncMock: + """Mocked notification client.""" + return AsyncMock() + + +@pytest.fixture +def publisher_notifier() -> AsyncMock: + """Mocked publisher notifier.""" + return AsyncMock() + + +@pytest.fixture +async def runs_publisher( + notification_client: AsyncMock, publisher_notifier: AsyncMock +) -> RunsPublisher: + """Instantiate RunsPublisher.""" + return RunsPublisher( + client=notification_client, publisher_notifier=publisher_notifier + ) + + +@pytest.mark.asyncio +async def test_initialize( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should initialize the runs_publisher with required parameters and callbacks.""" + run_id = "1234" + get_current_command = AsyncMock() + get_state_summary = AsyncMock() + + await runs_publisher.initialize(run_id, get_current_command, get_state_summary) + + assert runs_publisher._run_hooks + assert runs_publisher._run_hooks.run_id == run_id + assert runs_publisher._run_hooks.get_current_command == get_current_command + assert runs_publisher._run_hooks.get_state_summary == get_state_summary + assert runs_publisher._engine_state_slice + assert runs_publisher._engine_state_slice.current_command is None + assert runs_publisher._engine_state_slice.state_summary_status is None + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_clean_up_current_run( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should publish to appropriate topics at the end of a run.""" + await runs_publisher.initialize("1234", AsyncMock(), AsyncMock()) + + await runs_publisher.clean_up_current_run() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + notification_client.publish_advise_unsubscribe_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) + + +@pytest.mark.asyncio +async def test_handle_current_command_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle command changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._engine_state_slice.current_command = mock_curent_command("command1") + + await runs_publisher._handle_current_command_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_current_command = lambda _: mock_curent_command( + "command2" + ) + + await runs_publisher._handle_current_command_change() + + notification_client.publish_advise_refetch_async.assert_any_await( + topic=Topics.RUNS_CURRENT_COMMAND + ) + + +@pytest.mark.asyncio +async def test_handle_engine_status_change( + runs_publisher: RunsPublisher, notification_client: AsyncMock +) -> None: + """It should handle engine status changes appropriately.""" + await runs_publisher.initialize( + "1234", lambda _: mock_curent_command("command1"), AsyncMock() + ) + + assert runs_publisher._run_hooks + assert runs_publisher._engine_state_slice + + runs_publisher._run_hooks.run_id = "1234" + runs_publisher._run_hooks.get_state_summary = MagicMock( + return_value=MagicMock(status=EngineStatus.IDLE) + ) + runs_publisher._engine_state_slice.state_summary_status = EngineStatus.IDLE + + await runs_publisher._handle_engine_status_change() + + assert notification_client.publish_advise_refetch_async.call_count == 2 + + runs_publisher._run_hooks.get_state_summary.return_value = MagicMock( + status=EngineStatus.RUNNING + ) + + await runs_publisher._handle_engine_status_change() + + notification_client.publish_advise_refetch_async.assert_any_await(topic=Topics.RUNS) + notification_client.publish_advise_refetch_async.assert_any_await( + topic=f"{Topics.RUNS}/1234" + ) diff --git a/robot-server/tests/service/notifications/test_change_notifier.py b/robot-server/tests/service/notifications/test_change_notifier.py new file mode 100644 index 000000000000..4967e6d254ed --- /dev/null +++ b/robot-server/tests/service/notifications/test_change_notifier.py @@ -0,0 +1,56 @@ +"""Tests for the ChangeNotifier interface.""" +import asyncio +import pytest +from opentrons.protocol_engine.state.change_notifier import ChangeNotifier + + +async def test_single_subscriber() -> None: + """Test that a single subscriber can wait for a notification.""" + subject = ChangeNotifier() + result = asyncio.create_task(subject.wait()) + + # ensure that the wait actually waits by delaying and + # checking that the task has not resolved + await asyncio.sleep(0.1) + assert result.done() is False + + asyncio.get_running_loop().call_soon(subject.notify) + + await result + + +@pytest.mark.parametrize("_test_repetition", range(10)) +async def test_multiple_subscribers(_test_repetition: int) -> None: + """Test that multiple subscribers can wait for a notification. + + This test checks that the subscribers are awoken in the order they + subscribed. This may or may not be guarenteed according to the + implementations of both ChangeNotifier and the event loop. + This test functions as a canary, given that our code may relies + on this ordering for determinism. + + This test runs multiple times to check for flakyness. + """ + subject = ChangeNotifier() + results = [] + + async def _do_task_1() -> None: + await subject.wait() + results.append(1) + + async def _do_task_2() -> None: + await subject.wait() + results.append(2) + + async def _do_task_3() -> None: + await subject.wait() + results.append(3) + + task_1 = asyncio.create_task(_do_task_1()) + task_2 = asyncio.create_task(_do_task_2()) + task_3 = asyncio.create_task(_do_task_3()) + + asyncio.get_running_loop().call_soon(subject.notify) + await asyncio.gather(task_1, task_2, task_3) + + assert results == [1, 2, 3] diff --git a/robot-server/tests/service/notifications/test_publisher_notifier.py b/robot-server/tests/service/notifications/test_publisher_notifier.py new file mode 100644 index 000000000000..125cfdd18063 --- /dev/null +++ b/robot-server/tests/service/notifications/test_publisher_notifier.py @@ -0,0 +1,74 @@ +import asyncio +from unittest.mock import Mock, MagicMock + +from robot_server.service.notifications import ( + PublisherNotifier, + ChangeNotifier, +) + + +async def test_initialize() -> None: + """It should create a new task.""" + publisher_notifier = PublisherNotifier() + + await publisher_notifier._initialize() + + assert asyncio.get_running_loop() + + +def test_notify_publishers() -> None: + """Invoke the change notifier's notify method.""" + change_notifier = MagicMock() + publisher_notifier = PublisherNotifier(change_notifier) + + publisher_notifier._notify_publishers() + + change_notifier.notify.assert_called_once() + + +def test_register_publish_callbacks() -> None: + """It should extend the list of callbacks within a given list of callbacks.""" + publisher_notifier = PublisherNotifier() + callback1 = Mock() + callback2 = Mock() + + publisher_notifier.register_publish_callbacks([callback1, callback2]) + + assert len(publisher_notifier._callbacks) == 2 + assert publisher_notifier._callbacks[0] == callback1 + assert publisher_notifier._callbacks[1] == callback2 + + +async def test_wait_for_event() -> None: + """It should wait for an event to occur, then invoke each callback.""" + change_notifier = ChangeNotifier() + publisher_notifier = PublisherNotifier(change_notifier) + + callback_called = False + callback_2_called = False + + async def callback() -> None: + """Mock callback.""" + nonlocal callback_called + callback_called = True + + async def callback_2() -> None: + """Mock callback.""" + nonlocal callback_2_called + callback_2_called = True + + publisher_notifier.register_publish_callbacks([callback, callback_2]) + + async def trigger_callbacks() -> None: + """Mock trigger for callbacks.""" + await asyncio.sleep(0.1) + change_notifier.notify() + + task = asyncio.create_task(publisher_notifier._initialize()) + + await asyncio.gather(trigger_callbacks(), task) + + assert callback_called + assert callback_2_called + + task.cancel() diff --git a/scripts/deploy/create-release.js b/scripts/deploy/create-release.js index eb4db62bd2a0..3b804506a2ed 100644 --- a/scripts/deploy/create-release.js +++ b/scripts/deploy/create-release.js @@ -22,12 +22,6 @@ const parseArgs = require('./lib/parseArgs') const conventionalChangelog = require('conventional-changelog') const semver = require('semver') const { Octokit } = require('@octokit/rest') -const { - detailsFromTag, - tagFromDetails, - prefixForProject, - monorepoGit, -} = require('../git-version') const USAGE = '\nUsage:\n node ./scripts/deploy/create-release [--deploy] [--allow-old]' @@ -81,9 +75,35 @@ function versionPrevious(currentVersion, previousVersions) { return releasesOfGEQKind.length === 0 ? null : releasesOfGEQKind[0] } +async function gitVersion() { + let imported + if (imported === undefined) { + imported = await import('../git-version.mjs') + } + return imported +} + +async function monorepoGit() { + return await (await gitVersion()).monorepoGit() +} + +async function detailsFromTag(tag) { + return await (await gitVersion()).detailsFromTag(tag) +} + +async function tagFromDetails(project, version) { + return (await gitVersion()).tagFromDetails(project, version) +} + +async function prefixForProject(project) { + return (await gitVersion()).prefixForProject(project) +} + async function versionDetailsFromGit(tag, allowOld) { if (!allowOld) { - const last100 = await monorepoGit().log({ from: 'HEAD~100', to: 'HEAD' }) + const git = await monorepoGit() + const last100 = await git.log({ from: 'HEAD~100', to: 'HEAD' }) + if (!last100.all.some(commit => commit.refs.includes('tag: ' + tag))) { throw new Error( `Cannot find tag ${tag} in last 100 commits. You must run this script from a ref with ` + @@ -94,9 +114,8 @@ async function versionDetailsFromGit(tag, allowOld) { } const [project, currentVersion] = detailsFromTag(tag) - - const allTags = (await monorepoGit().tags([prefixForProject(project) + '*'])) - .all + const prefix = await prefixForProject(project) + const allTags = (await monorepoGit().tags([prefix + '*'])).all if (!allTags.includes(tag)) { throw new Error( `Tag ${tag} does not exist - create it before running this script` @@ -123,14 +142,15 @@ async function buildChangelog(project, currentVersion, previousVersion) { `## ${currentVersion}` + `\nFirst release for ${titleForProject(project)}` ) } - const previousTag = tagFromDetails(project, previousVersion) - + const previousTag = await tagFromDetails(project, previousVersion) + const currentTag = await tagFromDetails(project, currentVersion) + const prefix = await prefixForProject(Project) const changelogStream = conventionalChangelog( - { preset: 'angular', tagPrefix: prefixForProject(project) }, + { preset: 'angular', tagPrefix: prefix }, { version: currentVersion, - currentTag: tagFromDetails(project, currentVersion), - previousTag: previousTag, + currentTag, + previousTag, host: 'https://github.com', owner: REPO_DETAILS.owner, repository: REPO_DETAILS.repo, @@ -203,6 +223,7 @@ async function main() { currentVersion, previousVersion, ] = await versionDetailsFromGit(tag, allowOld) + const prefix = await prefixForProject(project) const changelog = await buildChangelog( project, currentVersion, @@ -211,8 +232,8 @@ async function main() { const truncatedChangelog = truncateAndAnnotate( changelog, 10000, - prefixForProject(project) + previousVersion, - prefixForProject(project) + currentVersion + prefix + previousVersion, + prefix + currentVersion ) return await createRelease( token, diff --git a/scripts/git-version.js b/scripts/git-version.mjs similarity index 79% rename from scripts/git-version.js rename to scripts/git-version.mjs index a2dab912f236..7b4d364d0da1 100644 --- a/scripts/git-version.js +++ b/scripts/git-version.mjs @@ -15,23 +15,24 @@ // What that all boils down to is that we need, and this module provides, an interface to get the version of a // given project that currently exists in the monorepo. -const git = require('simple-git') -const { dirname } = require('path') -const REPO_BASE = dirname(__dirname) +import git from 'simple-git' +import { dirname } from 'path' +import { fileURLToPath } from 'url' +const REPO_BASE = dirname(dirname(fileURLToPath(import.meta.url))) -function monorepoGit() { +export function monorepoGit() { return git({ baseDir: REPO_BASE }) } -const detailsFromTag = tag => +export const detailsFromTag = tag => tag.includes('@') ? tag.split('@') : ['robot-stack', tag.substring(1)] -function tagFromDetails(project, version) { +export function tagFromDetails(project, version) { const prefix = prefixForProject(project) return `${prefix}${version}` } -function prefixForProject(project) { +export function prefixForProject(project) { if (project === 'robot-stack') { return 'v' } else { @@ -39,7 +40,7 @@ function prefixForProject(project) { } } -async function latestTagForProject(project) { +export async function latestTagForProject(project) { return ( await monorepoGit().raw([ 'describe', @@ -50,7 +51,7 @@ async function latestTagForProject(project) { ).trim() } -async function versionForProject(project) { +export async function versionForProject(project) { return latestTagForProject(project) .then(tag => detailsFromTag(tag)[1]) .catch(error => { @@ -60,12 +61,3 @@ async function versionForProject(project) { return '0.0.0-dev' }) } - -module.exports = { - detailsFromTag, - tagFromDetails, - prefixForProject, - latestTagForProject, - versionForProject, - monorepoGit, -} diff --git a/scripts/update-releases-json.js b/scripts/update-releases-json.js index 3286256c42be..d7aa9b0ca215 100644 --- a/scripts/update-releases-json.js +++ b/scripts/update-releases-json.js @@ -4,8 +4,6 @@ const fs = require('fs/promises') // Updates a releases historical manifest with a release's version. -const versionFinder = require('./git-version') - const parseArgs = require('./deploy/lib/parseArgs') const USAGE = '\nUsage:\n node ./scripts/update-releases-json ' @@ -63,6 +61,7 @@ async function main() { } console.log(`Updating ${releasesPath} with artifacts from ${artifactDirPath}`) const releasesData = await readOrDefaultReleases(releasesPath) + const versionFinder = await import('./git-version.mjs') const version = await versionFinder.versionForProject(project) console.log(`Adding data for ${version}`) releasesData.production[version] = { diff --git a/setup-vitest.ts b/setup-vitest.ts index 07bd135137df..bf9d07a6ba79 100644 --- a/setup-vitest.ts +++ b/setup-vitest.ts @@ -10,6 +10,9 @@ vi.mock('./app/src/redux/shell/remote') process.env.OT_PD_VERSION = 'fake_PD_version' global._PKG_VERSION_ = 'test environment' +global._OPENTRONS_PROJECT_ = 'robotics' +global._PKG_PRODUCT_NAME_ = 'test product' +global._PKG_BUGS_URL_ = 'http://bugs.contoso.com' afterEach(() => { cleanup() diff --git a/shared-data/deck/definitions/5/ot2_short_trash.json b/shared-data/deck/definitions/5/ot2_short_trash.json new file mode 100644 index 000000000000..7d00f8d57731 --- /dev/null +++ b/shared-data/deck/definitions/5/ot2_short_trash.json @@ -0,0 +1,409 @@ +{ + "otId": "ot2_short_trash", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-115.65, -68.03, 0], + "dimensions": [624.3, 565.2, 0], + "metadata": { + "displayName": "OT-2 Short-Trash Deck", + "tags": ["ot2", "12 slots", "short trash"] + }, + "robot": { + "model": "OT-2 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 1", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 2", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 3", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "4", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 4", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "5", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 5", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "6", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 6", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "7", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 7", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "thermocyclerModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "8", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 8", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "9", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 9", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "10", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 10", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "11", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 11", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, + { + "id": "shortFixedTrash", + "areaType": "fixedTrash", + "offsetFromCutoutFixture": [29.285, -2.835, 0], + "boundingBox": { + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 58 + }, + "displayName": "Slot 12/Short Fixed Trash", + "ableToDropTips": true + } + ], + "cutouts": [ + { + "id": "cutout1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout 1" + }, + { + "id": "cutout2", + "position": [132.5, 0.0, 0.0], + "displayName": "Cutout 2" + }, + { + "id": "cutout3", + "position": [265.0, 0.0, 0.0], + "displayName": "Cutout 3" + }, + { + "id": "cutout4", + "position": [0.0, 90.5, 0.0], + "displayName": "Cutout 4" + }, + { + "id": "cutout5", + "position": [132.5, 90.5, 0.0], + "displayName": "Cutout 5" + }, + { + "id": "cutout6", + "position": [265.0, 90.5, 0.0], + "displayName": "Cutout 6" + }, + { + "id": "cutout7", + "position": [0.0, 181.0, 0.0], + "displayName": "Cutout 7" + }, + { + "id": "cutout8", + "position": [132.5, 181.0, 0.0], + "displayName": "Cutout 8" + }, + { + "id": "cutout9", + "position": [265.0, 181.0, 0.0], + "displayName": "Cutout 9" + }, + { + "id": "cutout10", + "position": [0.0, 271.5, 0.0], + "displayName": "Slot 10" + }, + { + "id": "cutout11", + "position": [132.5, 271.5, 0.0], + "displayName": "Cutout 11" + }, + { + "id": "cutout12", + "position": [265.0, 271.5, 0.0], + "displayName": "Cutout 12" + } + ], + "calibrationPoints": [ + { + "id": "1BLC", + "position": [12.13, 9.0, 0.0], + "displayName": "Slot 1 Bottom Left Cross" + }, + { + "id": "3BRC", + "position": [380.87, 9.0, 0.0], + "displayName": "Slot 3 Bottom Right Cross" + }, + { + "id": "7TLC", + "position": [12.13, 258.0, 0.0], + "displayName": "Slot 7 Top Left Cross" + }, + { + "id": "9TRC", + "position": [380.87, 258.0, 0.0], + "displayName": "Slot 9 Top Right Cross" + }, + { + "id": "10TLC", + "position": [12.13, 348.5, 0.0], + "displayName": "Slot 10 Top Left Cross" + }, + { + "id": "11TRC", + "position": [248.37, 348.5, 0.0], + "displayName": "Slot 11 Top Right Cross" + }, + { + "id": "1BLD", + "position": [12.13, 6.0, 0.0], + "displayName": "Slot 1 Bottom Left Dot" + }, + { + "id": "3BRD", + "position": [380.87, 6.0, 0.0], + "displayName": "Slot 3 Bottom Right Dot" + }, + { + "id": "7TLD", + "position": [12.13, 261.0, 0.0], + "displayName": "Slot 7 Top Left Dot" + }, + { + "id": "9TRD", + "position": [380.87, 261.0, 0.0], + "displayName": "Slot 9 Top Right Dot" + }, + { + "id": "10TLD", + "position": [12.13, 351.5, 0.0], + "displayName": "Slot 10 Top Left Dot" + }, + { + "id": "11TRD", + "position": [248.37, 351.5, 0.0], + "displayName": "Slot 11 Top Right Dot" + } + ], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "12", + "labware": "opentrons_1_trash_850ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleStandardSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutout1", + "cutout2", + "cutout3", + "cutout4", + "cutout5", + "cutout6", + "cutout7", + "cutout8", + "cutout9", + "cutout10", + "cutout11", + "cutout12" + ], + "displayName": "Standard Slot", + "providesAddressableAreas": { + "cutout1": ["1"], + "cutout2": ["2"], + "cutout3": ["3"], + "cutout4": ["4"], + "cutout5": ["5"], + "cutout6": ["6"], + "cutout7": ["7"], + "cutout8": ["8"], + "cutout9": ["9"], + "cutout10": ["10"], + "cutout11": ["11"], + "cutout12": ["12"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "fixedTrashSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutout12"], + "displayName": "Fixed Trash", + "providesAddressableAreas": { + "cutout12": ["shortFixedTrash"] + }, + "fixtureGroup": {}, + "height": 58 + } + ] +} diff --git a/shared-data/deck/definitions/5/ot2_standard.json b/shared-data/deck/definitions/5/ot2_standard.json new file mode 100644 index 000000000000..0ccc1997c3e6 --- /dev/null +++ b/shared-data/deck/definitions/5/ot2_standard.json @@ -0,0 +1,409 @@ +{ + "otId": "ot2_standard", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-115.65, -68.03, 0], + "dimensions": [624.3, 565.2, 0], + "metadata": { + "displayName": "OT-2 Standard Deck", + "tags": ["ot2", "12 slots", "standard"] + }, + "robot": { + "model": "OT-2 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 1", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 2", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 3", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "4", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 4", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "5", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 5", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "6", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 6", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "7", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 7", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "thermocyclerModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "8", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 8", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "9", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 9", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "10", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 10", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "11", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 11", + "compatibleModuleTypes": [ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType" + ] + }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, + { + "id": "fixedTrash", + "areaType": "fixedTrash", + "offsetFromCutoutFixture": [29.285, -2.835, 0], + "boundingBox": { + "xDimension": 107.11, + "yDimension": 165.67, + "zDimension": 82 + }, + "displayName": "Slot 12/Fixed Trash", + "ableToDropTips": true + } + ], + "cutouts": [ + { + "id": "cutout1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout 1" + }, + { + "id": "cutout2", + "position": [132.5, 0.0, 0.0], + "displayName": "Cutout 2" + }, + { + "id": "cutout3", + "position": [265.0, 0.0, 0.0], + "displayName": "Cutout 3" + }, + { + "id": "cutout4", + "position": [0.0, 90.5, 0.0], + "displayName": "Cutout 4" + }, + { + "id": "cutout5", + "position": [132.5, 90.5, 0.0], + "displayName": "Cutout 5" + }, + { + "id": "cutout6", + "position": [265.0, 90.5, 0.0], + "displayName": "Cutout 6" + }, + { + "id": "cutout7", + "position": [0.0, 181.0, 0.0], + "displayName": "Cutout 7" + }, + { + "id": "cutout8", + "position": [132.5, 181.0, 0.0], + "displayName": "Cutout 8" + }, + { + "id": "cutout9", + "position": [265.0, 181.0, 0.0], + "displayName": "Cutout 9" + }, + { + "id": "cutout10", + "position": [0.0, 271.5, 0.0], + "displayName": "Slot 10" + }, + { + "id": "cutout11", + "position": [132.5, 271.5, 0.0], + "displayName": "Cutout 11" + }, + { + "id": "cutout12", + "position": [265.0, 271.5, 0.0], + "displayName": "Cutout 12" + } + ], + "calibrationPoints": [ + { + "id": "1BLC", + "position": [12.13, 9.0, 0.0], + "displayName": "Slot 1 Bottom Left Cross" + }, + { + "id": "3BRC", + "position": [380.87, 9.0, 0.0], + "displayName": "Slot 3 Bottom Right Cross" + }, + { + "id": "7TLC", + "position": [12.13, 258.0, 0.0], + "displayName": "Slot 7 Top Left Cross" + }, + { + "id": "9TRC", + "position": [380.87, 258.0, 0.0], + "displayName": "Slot 9 Top Right Cross" + }, + { + "id": "10TLC", + "position": [12.13, 348.5, 0.0], + "displayName": "Slot 10 Top Left Cross" + }, + { + "id": "11TRC", + "position": [248.37, 348.5, 0.0], + "displayName": "Slot 11 Top Right Cross" + }, + { + "id": "1BLD", + "position": [12.13, 6.0, 0.0], + "displayName": "Slot 1 Bottom Left Dot" + }, + { + "id": "3BRD", + "position": [380.87, 6.0, 0.0], + "displayName": "Slot 3 Bottom Right Dot" + }, + { + "id": "7TLD", + "position": [12.13, 261.0, 0.0], + "displayName": "Slot 7 Top Left Dot" + }, + { + "id": "9TRD", + "position": [380.87, 261.0, 0.0], + "displayName": "Slot 9 Top Right Dot" + }, + { + "id": "10TLD", + "position": [12.13, 351.5, 0.0], + "displayName": "Slot 10 Top Left Dot" + }, + { + "id": "11TRD", + "position": [248.37, 351.5, 0.0], + "displayName": "Slot 11 Top Right Dot" + } + ], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "12", + "labware": "opentrons_1_trash_1100ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleStandardSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutout1", + "cutout2", + "cutout3", + "cutout4", + "cutout5", + "cutout6", + "cutout7", + "cutout8", + "cutout9", + "cutout10", + "cutout11", + "cutout12" + ], + "displayName": "Standard Slot", + "providesAddressableAreas": { + "cutout1": ["1"], + "cutout2": ["2"], + "cutout3": ["3"], + "cutout4": ["4"], + "cutout5": ["5"], + "cutout6": ["6"], + "cutout7": ["7"], + "cutout8": ["8"], + "cutout9": ["9"], + "cutout10": ["10"], + "cutout11": ["11"], + "cutout12": ["12"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "fixedTrashSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutout12"], + "displayName": "Fixed Trash", + "providesAddressableAreas": { + "cutout12": ["fixedTrash"] + }, + "fixtureGroup": {}, + "height": 82 + } + ] +} diff --git a/shared-data/deck/definitions/5/ot3_standard.json b/shared-data/deck/definitions/5/ot3_standard.json new file mode 100644 index 000000000000..85dcbf64792e --- /dev/null +++ b/shared-data/deck/definitions/5/ot3_standard.json @@ -0,0 +1,1042 @@ +{ + "otId": "ot3_standard", + "schemaVersion": 5, + "cornerOffsetFromOrigin": [-204.31, -76.59, 0], + "dimensions": [854.995, 581.74, 0], + "metadata": { + "displayName": "OT-3 Standard Deck", + "tags": ["ot3", "12 slots", "standard"] + }, + "robot": { + "model": "OT-3 Standard" + }, + "locations": { + "addressableAreas": [ + { + "id": "D1", + "areaType": "slot", + "matingSurfaceUnitVector": [-1, 1, -1], + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D1" + }, + { + "id": "D2", + "areaType": "slot", + "matingSurfaceUnitVector": [-1, 1, -1], + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D2" + }, + { + "id": "D3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D3" + }, + { + "id": "C1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C1" + }, + { + "id": "C2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C2" + }, + { + "id": "C3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C3" + }, + { + "id": "B1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B1" + }, + { + "id": "B2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B2" + }, + { + "id": "B3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B3" + }, + { + "id": "A1", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A1" + }, + { + "id": "A2", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A2", + "compatibleModuleTypes": [] + }, + { + "id": "A3", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A3", + "compatibleModuleTypes": [] + }, + { + "id": "A4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot A4", + "compatibleModuleTypes": [] + }, + { + "id": "B4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot B4", + "compatibleModuleTypes": [] + }, + { + "id": "C4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot C4", + "compatibleModuleTypes": [] + }, + { + "id": "D4", + "areaType": "stagingSlot", + "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot D4", + "compatibleModuleTypes": [] + }, + { + "id": "movableTrashD1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in D1", + "ableToDropTips": true + }, + { + "id": "movableTrashC1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in C1", + "ableToDropTips": true + }, + { + "id": "movableTrashB1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in B1", + "ableToDropTips": true + }, + { + "id": "movableTrashA1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-90.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in A1", + "ableToDropTips": true + }, + { + "id": "movableTrashD3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in D3", + "ableToDropTips": true + }, + { + "id": "movableTrashC3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in C3", + "ableToDropTips": true + }, + { + "id": "movableTrashB3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in B3", + "ableToDropTips": true + }, + { + "id": "movableTrashA3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-6.25, 4, 0.0], + "boundingBox": { + "xDimension": 225, + "yDimension": 78, + "zDimension": 40 + }, + "displayName": "Trash Bin in A3", + "ableToDropTips": true + }, + { + "id": "1ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, 36, 114.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 0, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "8ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, -27, 114.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 63, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "96ChannelWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [14.445, -20.915, 114.5], + "boundingBox": { + "xDimension": 99, + "yDimension": 63, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropTips": true + }, + { + "id": "gripperWasteChute", + "areaType": "wasteChute", + "offsetFromCutoutFixture": [64, 29, 136.5], + "boundingBox": { + "xDimension": 0, + "yDimension": 0, + "zDimension": 0 + }, + "displayName": "Waste Chute", + "ableToDropLabware": true + }, + { + "id": "thermocyclerModuleV2", + "areaType": "thermocycler", + "offsetFromCutoutFixture": [-20.005, 67.96, 10.96], + "matingSurfaceUnitVector": [-1, 1, -1], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Thermocycler Module Slot" + }, + { + "id": "heaterShakerV1D1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0, 0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D1" + }, + { + "id": "heaterShakerV1C1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C1" + }, + { + "id": "heaterShakerV1B1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B1" + }, + { + "id": "heaterShakerV1A1", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A1" + }, + { + "id": "heaterShakerV1D3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in D3" + }, + { + "id": "heaterShakerV1C3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in C3" + }, + { + "id": "heaterShakerV1B3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in B3" + }, + { + "id": "heaterShakerV1A3", + "areaType": "heaterShaker", + "offsetFromCutoutFixture": [0.0, 0.0, 18.95], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Heater Shaker in A3" + }, + { + "id": "temperatureModuleV2D1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D1" + }, + { + "id": "temperatureModuleV2C1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C1" + }, + { + "id": "temperatureModuleV2B1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B1" + }, + { + "id": "temperatureModuleV2A1", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A1" + }, + { + "id": "temperatureModuleV2D3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in D3" + }, + { + "id": "temperatureModuleV2C3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in C3" + }, + { + "id": "temperatureModuleV2B3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in B3" + }, + { + "id": "temperatureModuleV2A3", + "areaType": "temperatureModule", + "offsetFromCutoutFixture": [0.0, 0.0, 9.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Temperature Module in A3" + }, + { + "id": "magneticBlockV1D1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D1" + }, + { + "id": "magneticBlockV1C1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C1" + }, + { + "id": "magneticBlockV1B1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B1" + }, + { + "id": "magneticBlockV1A1", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A1" + }, + { + "id": "magneticBlockV1D2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D2" + }, + { + "id": "magneticBlockV1C2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C2" + }, + { + "id": "magneticBlockV1B2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B2" + }, + { + "id": "magneticBlockV1A2", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A2" + }, + { + "id": "magneticBlockV1D3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in D3" + }, + { + "id": "magneticBlockV1C3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in C3" + }, + { + "id": "magneticBlockV1B3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in B3" + }, + { + "id": "magneticBlockV1A3", + "areaType": "magneticBlock", + "offsetFromCutoutFixture": [0.0, 0.0, 38], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Magnetic Block in A3" + } + ], + "cutouts": [ + { + "id": "cutoutD1", + "position": [0.0, 0.0, 0.0], + "displayName": "Cutout D1" + }, + { + "id": "cutoutD2", + "position": [164.0, 0.0, 0.0], + "displayName": "Cutout D2" + }, + { + "id": "cutoutD3", + "position": [328.0, 0.0, 0.0], + "displayName": "Cutout D3" + }, + { + "id": "cutoutC1", + "position": [0.0, 107, 0.0], + "displayName": "Cutout C1" + }, + { + "id": "cutoutC2", + "position": [164.0, 107, 0.0], + "displayName": "Cutout C2" + }, + { + "id": "cutoutC3", + "position": [328.0, 107, 0.0], + "displayName": "Cutout C3" + }, + { + "id": "cutoutB1", + "position": [0.0, 214.0, 0.0], + "displayName": "Cutout B1" + }, + { + "id": "cutoutB2", + "position": [164.0, 214.0, 0.0], + "displayName": "Cutout B2" + }, + { + "id": "cutoutB3", + "position": [328.0, 214.0, 0.0], + "displayName": "Cutout B3" + }, + { + "id": "cutoutA1", + "position": [0.0, 321.0, 0.0], + "displayName": "Cutout A1" + }, + { + "id": "cutoutA2", + "position": [164.0, 321.0, 0.0], + "displayName": "Cutout A2" + }, + { + "id": "cutoutA3", + "position": [328.0, 321.0, 0.0], + "displayName": "Cutout A3" + } + ], + "calibrationPoints": [], + "legacyFixtures": [ + { + "id": "fixedTrash", + "slot": "A3", + "labware": "opentrons_1_trash_3200ml_fixed", + "displayName": "Fixed Trash" + } + ] + }, + "cutoutFixtures": [ + { + "id": "singleLeftSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD1", "cutoutC1", "cutoutB1", "cutoutA1"], + "displayName": "Standard Slot Left", + "providesAddressableAreas": { + "cutoutD1": ["D1"], + "cutoutC1": ["C1"], + "cutoutB1": ["B1"], + "cutoutA1": ["A1"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "singleCenterSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD2", "cutoutC2", "cutoutB2", "cutoutA2"], + "displayName": "Standard Slot Center", + "providesAddressableAreas": { + "cutoutD2": ["D2"], + "cutoutC2": ["C2"], + "cutoutB2": ["B2"], + "cutoutA2": ["A2"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "singleRightSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Standard Slot Right", + "providesAddressableAreas": { + "cutoutD3": ["D3"], + "cutoutC3": ["C3"], + "cutoutB3": ["B3"], + "cutoutA3": ["A3"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "stagingAreaRightSlot", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": ["D3", "D4"], + "cutoutC3": ["C3", "C4"], + "cutoutB3": ["B3", "B4"], + "cutoutA3": ["A3", "A4"] + }, + "fixtureGroup": {}, + "height": 0 + }, + { + "id": "trashBinAdapter", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With Movable Trash", + "providesAddressableAreas": { + "cutoutD1": ["movableTrashD1"], + "cutoutC1": ["movableTrashC1"], + "cutoutB1": ["movableTrashB1"], + "cutoutA1": ["movableTrashA1"], + "cutoutD3": ["movableTrashD3"], + "cutoutC3": ["movableTrashC3"], + "cutoutB3": ["movableTrashB3"], + "cutoutA3": ["movableTrashA3"] + }, + "fixtureGroup": {}, + "height": 40 + }, + { + "id": "wasteChuteRightAdapterCovered", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Waste Chute Adapter for 1 or 8 Channel Pipettes", + "providesAddressableAreas": { + "cutoutD3": ["1ChannelWasteChute", "8ChannelWasteChute"] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "wasteChuteRightAdapterNoCover", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Waste Chute Adapter for 96 Channel Pipette or Gripper", + "providesAddressableAreas": { + "cutoutD3": [ + "1ChannelWasteChute", + "8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute" + ] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "stagingAreaSlotWithWasteChuteRightAdapterCovered", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Staging Slot With Waste Chute Adapter for 96 Channel Pipette or Gripper", + "providesAddressableAreas": { + "cutoutD3": ["1ChannelWasteChute", "8ChannelWasteChute", "D4"] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "stagingAreaSlotWithWasteChuteRightAdapterNoCover", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3"], + "displayName": "Staging Slot With Waste Chute Adapter and Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": [ + "1ChannelWasteChute", + "8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute", + "D4" + ] + }, + "fixtureGroup": {}, + "height": 124.5 + }, + { + "id": "thermocyclerModuleV2Rear", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": ["cutoutA1"], + "displayName": "Rear Slot portion of the Thermocycler Moduler", + "providesAddressableAreas": { + "cutoutA1": [] + }, + "fixtureGroup": { + "cutoutA1": [ + { + "cutoutA1": "thermocyclerModuleV2Rear", + "cutoutB1": "thermocyclerModuleV2Front" + } + ] + }, + "height": 72.35 + }, + { + "id": "thermocyclerModuleV2Front", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": ["cutoutB1"], + "displayName": "Front Slot portion of the Thermocycler Moduler", + "providesAddressableAreas": { + "cutoutB1": ["thermocyclerModuleV2"] + }, + "fixtureGroup": { + "cutoutB1": [ + { + "cutoutA1": "thermocyclerModuleV2Rear", + "cutoutB1": "thermocyclerModuleV2Front" + } + ] + }, + "height": 72.35 + }, + { + "id": "heaterShakerModuleV1", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Heater Shaker", + "providesAddressableAreas": { + "cutoutD1": ["heaterShakerV1D1"], + "cutoutC1": ["heaterShakerV1C1"], + "cutoutB1": ["heaterShakerV1B1"], + "cutoutA1": ["heaterShakerV1A1"], + "cutoutD3": ["heaterShakerV1D3"], + "cutoutC3": ["heaterShakerV1C3"], + "cutoutB3": ["heaterShakerV1B3"], + "cutoutA3": ["heaterShakerV1A3"] + }, + "fixtureGroup": {}, + "height": 18.95 + }, + { + "id": "temperatureModuleV2", + "expectOpentronsModuleSerialNumber": true, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Temperature Module", + "providesAddressableAreas": { + "cutoutD1": ["temperatureModuleV2D1"], + "cutoutC1": ["temperatureModuleV2C1"], + "cutoutB1": ["temperatureModuleV2B1"], + "cutoutA1": ["temperatureModuleV2A1"], + "cutoutD3": ["temperatureModuleV2D3"], + "cutoutC3": ["temperatureModuleV2C3"], + "cutoutB3": ["temperatureModuleV2B3"], + "cutoutA3": ["temperatureModuleV2A3"] + }, + "fixtureGroup": {}, + "height": 9.0 + }, + { + "id": "magneticBlockV1", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": [ + "cutoutD1", + "cutoutC1", + "cutoutB1", + "cutoutA1", + "cutoutD2", + "cutoutC2", + "cutoutB2", + "cutoutA2", + "cutoutD3", + "cutoutC3", + "cutoutB3", + "cutoutA3" + ], + "displayName": "Slot With a Magnetic Block", + "providesAddressableAreas": { + "cutoutD1": ["magneticBlockV1D1"], + "cutoutC1": ["magneticBlockV1C1"], + "cutoutB1": ["magneticBlockV1B1"], + "cutoutA1": ["magneticBlockV1A1"], + "cutoutD2": ["magneticBlockV1D2"], + "cutoutC2": ["magneticBlockV1C2"], + "cutoutB2": ["magneticBlockV1B2"], + "cutoutA2": ["magneticBlockV1A2"], + "cutoutD3": ["magneticBlockV1D3"], + "cutoutC3": ["magneticBlockV1C3"], + "cutoutB3": ["magneticBlockV1B3"], + "cutoutA3": ["magneticBlockV1A3"] + }, + "fixtureGroup": {}, + "height": 38.0 + }, + { + "id": "stagingAreaSlotWithMagneticBlockV1", + "expectOpentronsModuleSerialNumber": false, + "mayMountTo": ["cutoutD3", "cutoutC3", "cutoutB3", "cutoutA3"], + "displayName": "Fixture that provides a Magnetic Block and a Staging Area Slot", + "providesAddressableAreas": { + "cutoutD3": ["magneticBlockV1D3", "D4"], + "cutoutC3": ["magneticBlockV1C3", "C4"], + "cutoutB3": ["magneticBlockV1B3", "B4"], + "cutoutA3": ["magneticBlockV1A3", "A4"] + }, + "fixtureGroup": {}, + "height": 38.0 + } + ], + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": -0.75 + } + } + } +} diff --git a/shared-data/deck/index.ts b/shared-data/deck/index.ts index e308d7a17ad5..7d68bdeebd93 100644 --- a/shared-data/deck/index.ts +++ b/shared-data/deck/index.ts @@ -8,11 +8,16 @@ import ot2StandardDeckV4 from './definitions/4/ot2_standard.json' import ot2ShortFixedTrashDeckV4 from './definitions/4/ot2_short_trash.json' import ot3StandardDeckV4 from './definitions/4/ot3_standard.json' +// v5 deck defs +import ot2StandardDeckV5 from './definitions/5/ot2_standard.json' +import ot2ShortFixedTrashDeckV5 from './definitions/5/ot2_short_trash.json' +import ot3StandardDeckV5 from './definitions/5/ot3_standard.json' + import deckExample from './fixtures/3/deckExample.json' import type { DeckDefinition } from '../js/types' -export * from './types/schemaV4' +export * from './types/schemaV5' export { ot2StandardDeckV3, @@ -21,13 +26,16 @@ export { ot2StandardDeckV4, ot2ShortFixedTrashDeckV4, ot3StandardDeckV4, + ot2StandardDeckV5, + ot2ShortFixedTrashDeckV5, + ot3StandardDeckV5, deckExample, } const latestDeckDefinitions = { - ot2StandardDeckV4, - ot2ShortFixedTrashDeckV4, - ot3StandardDeckV4, + ot2StandardDeckV5, + ot2ShortFixedTrashDeckV5, + ot3StandardDeckV5, } export function getDeckDefinitions(): Record { diff --git a/shared-data/deck/schemas/5.json b/shared-data/deck/schemas/5.json new file mode 100644 index 000000000000..da77152da13e --- /dev/null +++ b/shared-data/deck/schemas/5.json @@ -0,0 +1,338 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "opentronsDeckSchemaV5", + "definitions": { + "positiveNumber": { + "type": "number", + "minimum": 0 + }, + "xyzArray": { + "type": "array", + "description": "Array of 3 numbers, [x, y, z]", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + }, + "coordinates": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y", "z"], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "unitVector": { + "type": "array", + "description": "Array of 3 unit directions, [x, y, z]", + "items": { + "type": "number", + "enum": [1, -1] + }, + "minItems": 3, + "maxItems": 3 + }, + "boundingBox": { + "type": "object", + "required": ["xDimension", "yDimension", "zDimension"], + "properties": { + "xDimension": { "$ref": "#/definitions/positiveNumber" }, + "yDimension": { "$ref": "#/definitions/positiveNumber" }, + "zDimension": { "$ref": "#/definitions/positiveNumber" } + } + } + }, + "description": "Deck specifications, where x,y,z (0,0,0) is at front the bottom left corner.", + "type": "object", + "additionalProperties": false, + "required": [ + "otId", + "schemaVersion", + "cornerOffsetFromOrigin", + "dimensions", + "metadata", + "robot", + "locations", + "cutoutFixtures" + ], + "properties": { + "otId": { + "description": "Unique internal ID generated using UUID", + "type": "string" + }, + "schemaVersion": { + "description": "Schema version of a deck is a single integer", + "enum": [5] + }, + "cornerOffsetFromOrigin": { + "$ref": "#/definitions/xyzArray", + "description": "Position of left-front-bottom corner of entire deck to robot coordinate system origin" + }, + "dimensions": { + "$ref": "#/definitions/xyzArray", + "description": "Outer dimensions of a deck bounding box" + }, + "metadata": { + "description": "Optional metadata about the Deck", + "type": "object", + "properties": { + "displayName": { + "description": "A short, human-readable name for the deck", + "type": "string" + }, + "tags": { + "description": "Tags to be used in searching for this deck", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "robot": { + "type": "object", + "required": ["model"], + "properties": { + "model": { + "description": "Model of the robot", + "type": "string", + "enum": ["OT-2 Standard", "OT-3 Standard"] + } + } + }, + "locations": { + "type": "object", + "required": [ + "addressableAreas", + "calibrationPoints", + "cutouts", + "legacyFixtures" + ], + "properties": { + "addressableAreas": { + "type": "array", + "items": { + "type": "object", + "description": "An addressable area is a named area in 3D space that the robot can interact with--for example, as a place to drop tips, or hold a labware.", + "required": [ + "id", + "areaType", + "offsetFromCutoutFixture", + "boundingBox", + "displayName" + ], + "properties": { + "id": { + "description": "Unique identifier for slot", + "type": "string" + }, + "areaType": { + "description": "The type of addressable area, defining allowed behavior.", + "type": "string", + "enum": [ + "slot", + "stagingSlot", + "movableTrash", + "fixedTrash", + "wasteChute" + ] + }, + "offsetFromCutoutFixture": { + "$ref": "#/definitions/xyzArray", + "description": "The offset from the origin of the cutout fixture that's providing this addressable area (which is currently identical to the position of the underlying cutout), to the -x, -y, -z corner of this addressable area's bounding box." + }, + "matingSurfaceUnitVector": { + "$ref": "#/definitions/unitVector", + "description": "An optional diagonal direction of force, defined by spring location, which governs the mating surface of objects placed in this addressable area. Meant to be used when this addressable area is a slot." + }, + "boundingBox": { + "description": "The active area (both pipettes can reach) of this addressable area.", + "$ref": "#/definitions/boundingBox" + }, + "displayName": { + "description": "A human-readable nickname for this area e.g. \"Slot A1\" or \"Trash Bin in A1\"", + "type": "string" + }, + "compatibleModuleTypes": { + "description": "OT-2 Only parameter. An array of module types that can be placed in this area. The module type names can be found in the moduleType field of a module definition.", + "type": "array", + "items": { + "type": "string" + } + }, + "ableToDropTips": { + "description": "Whether tips are allowed to be dropped into this area. If `true`, the top-center of the `boundingBox` should be a good location for the bottom-center of all the tips when they're dropped.", + "type": "boolean" + }, + "ableToDropLabware": { + "description": "Whether labware is allowed to be dropped (different from being placed) into this area. If `true`, the top-center of the `boundingBox` should be a good location for the bottom-center of the labware when it's dropped.", + "type": "boolean" + } + } + } + }, + "calibrationPoints": { + "type": "array", + "description": "Key points for deck calibration", + "items": { + "type": "object", + "required": ["id", "position", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for calibration point", + "type": "string" + }, + "position": { + "$ref": "#/definitions/xyzArray" + }, + "displayName": { + "description": "An optional human-readable nickname for this point Eg \"Slot 3 Cross\" or \"Slot 1 Dot\"", + "type": "string" + } + } + } + }, + "cutouts": { + "type": "array", + "description": "The machined cutout slots on the deck surface.", + "items": { + "type": "object", + "required": ["id", "position", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for the cutout", + "type": "string" + }, + "position": { + "description": "Absolute position of the cutout", + "$ref": "#/definitions/xyzArray" + }, + "displayName": { + "description": "An optional human-readable nickname for this cutout e.g. \"Cutout A1\"", + "type": "string" + } + } + } + }, + "legacyFixtures": { + "type": "array", + "description": "Fixed position objects on the deck.", + "items": { + "type": "object", + "required": ["id", "displayName"], + "properties": { + "id": { + "description": "Unique identifier for fixed object", + "type": "string" + }, + "labware": { + "description": "Valid labware loadName for fixed object", + "type": "string" + }, + "slot": { + "description": "Slot location of the fixed object", + "type": "string" + }, + "displayName": { + "description": "An optional human-readable nickname for this fixture Eg \"Tall Fixed Trash\" or \"Short Fixed Trash\"", + "type": "string" + } + } + } + } + } + }, + "cutoutFixtures": { + "type": "array", + "items": { + "description": "A cutout fixture is a physical thing that can be mounted onto one of the deck cutouts.", + "type": "object", + "required": [ + "id", + "expectOpentronsModuleSerialNumber", + "mayMountTo", + "displayName", + "providesAddressableAreas", + "fixtureGroup", + "height" + ], + "properties": { + "id": { + "description": "Unique identifier for the cutout fixture.", + "type": "string" + }, + "expectOpentronsModuleSerialNumber": { + "description": "Determines whether or not a fixture expects a serial number for a connected Opentrons Module.", + "type": "boolean" + }, + "mayMountTo": { + "description": "A list of compatible cutouts this fixture may be mounted to. These must match `id`s in `cutouts`.", + "type": "array", + "items": { + "type": "string" + } + }, + "displayName": { + "description": "A human-readable nickname for this area e.g. \"Standard Right Slot\" or \"Slot With Movable Trash\"", + "type": "string" + }, + "providesAddressableAreas": { + "description": "The addressable areas that this cutout fixture provides, when it's mounted. It can provide different addressable areas depending on where it's mounted. Keys must match values from this object's `mayMountTo`. Values must match `id`s from `addressableAreas`.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "fixtureGroup": { + "description": "The map of fixtures that must exist in the deck configuration if this fixture exists, with the mounting location acting as a key to determine the location of the rest of the group.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "height": { + "description": "The vertical distance (mm) from the cutout fixture's origin to its tallest physical feature that an instrument could collide with.", + "type": "number" + } + } + } + }, + "gripperOffsets": { + "type": "object", + "description": "Offsets to be added when calculating the coordinates a gripper should go to when picking up or dropping a labware on this deck.", + "properties": { + "default": { + "type": "object", + "properties": { + "pickUpOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate pick-up coordinates of a labware placed on this deck." + }, + "dropOffset": { + "$ref": "#/definitions/coordinates", + "description": "Offset added to calculate drop coordinates of a labware placed on this deck." + } + }, + "required": ["pickUpOffset", "dropOffset"] + } + }, + "required": ["default"] + } + } +} diff --git a/shared-data/deck/types/schemaV5.ts b/shared-data/deck/types/schemaV5.ts new file mode 100644 index 000000000000..e763b893bdeb --- /dev/null +++ b/shared-data/deck/types/schemaV5.ts @@ -0,0 +1,141 @@ +export type FlexAddressableAreaName = + | 'A1' + | 'B1' + | 'C1' + | 'D1' + | 'A2' + | 'B2' + | 'C2' + | 'D2' + | 'A3' + | 'B3' + | 'C3' + | 'D3' + | 'A4' + | 'B4' + | 'C4' + | 'D4' + | 'movableTrashA1' + | 'movableTrashB1' + | 'movableTrashC1' + | 'movableTrashD1' + | 'movableTrashA3' + | 'movableTrashB3' + | 'movableTrashC3' + | 'movableTrashD3' + | '1ChannelWasteChute' + | '8ChannelWasteChute' + | '96ChannelWasteChute' + | 'gripperWasteChute' + | 'thermocyclerModuleV2' + | 'heaterShakerV1A1' + | 'heaterShakerV1B1' + | 'heaterShakerV1C1' + | 'heaterShakerV1D1' + | 'heaterShakerV1A3' + | 'heaterShakerV1B3' + | 'heaterShakerV1C3' + | 'heaterShakerV1D3' + | 'temperatureModuleV2A1' + | 'temperatureModuleV2B1' + | 'temperatureModuleV2C1' + | 'temperatureModuleV2D1' + | 'temperatureModuleV2A3' + | 'temperatureModuleV2B3' + | 'temperatureModuleV2C3' + | 'temperatureModuleV2D3' + | 'magneticBlockV1A1' + | 'magneticBlockV1B1' + | 'magneticBlockV1C1' + | 'magneticBlockV1D1' + | 'magneticBlockV1A2' + | 'magneticBlockV1B2' + | 'magneticBlockV1C2' + | 'magneticBlockV1D2' + | 'magneticBlockV1A3' + | 'magneticBlockV1B3' + | 'magneticBlockV1C3' + | 'magneticBlockV1D3' + +export type OT2AddressableAreaName = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | '12' + | 'fixedTrash' + +export type AddressableAreaName = + | FlexAddressableAreaName + | OT2AddressableAreaName + +export type CutoutId = + | 'cutoutD1' + | 'cutoutD2' + | 'cutoutD3' + | 'cutoutC1' + | 'cutoutC2' + | 'cutoutC3' + | 'cutoutB1' + | 'cutoutB2' + | 'cutoutB3' + | 'cutoutA1' + | 'cutoutA2' + | 'cutoutA3' + +export type OT2CutoutId = + | 'cutout1' + | 'cutout2' + | 'cutout3' + | 'cutout4' + | 'cutout5' + | 'cutout6' + | 'cutout7' + | 'cutout8' + | 'cutout9' + | 'cutout10' + | 'cutout11' + | 'cutout12' + +export type SingleSlotCutoutFixtureId = + | 'singleLeftSlot' + | 'singleCenterSlot' + | 'singleRightSlot' + +export type StagingAreaRightSlotFixtureId = 'stagingAreaRightSlot' + +export type TrashBinAdapterCutoutFixtureId = 'trashBinAdapter' + +export type WasteChuteCutoutFixtureId = + | 'wasteChuteRightAdapterCovered' + | 'wasteChuteRightAdapterNoCover' + | 'stagingAreaSlotWithWasteChuteRightAdapterCovered' + | 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' + +export type FlexModuleCutoutFixtureId = + | 'heaterShakerModuleV1' + | 'temperatureModuleV2' + | 'magneticBlockV1' + | 'stagingAreaSlotWithMagneticBlockV1' + | 'thermocyclerModuleV2Rear' + | 'thermocyclerModuleV2Front' + +export type OT2SingleStandardSlot = 'singleStandardSlot' + +export type OT2FixedTrashSlot = 'fixedTrashSlot' + +export type CutoutFixtureId = + | SingleSlotCutoutFixtureId + | StagingAreaRightSlotFixtureId + | TrashBinAdapterCutoutFixtureId + | WasteChuteCutoutFixtureId + | FlexModuleCutoutFixtureId + | OT2SingleStandardSlot + | OT2FixedTrashSlot diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index 6eae38eba667..15c72cd98821 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -158,6 +158,7 @@ describe('pipette data accessors', () => { minVolume: 5, supportedTips: { t50: { + uiMaxFlowRate: 47, aspirate: { default: { 1: expect.anything(), @@ -205,27 +206,28 @@ describe('pipette data accessors', () => { minVolume: 1, supportedTips: { t50: { + uiMaxFlowRate: 26.7, aspirate: { default: { 1: expect.anything(), }, }, defaultAspirateFlowRate: { - default: 35, + default: 26.7, valuesByApiLevel: { - 2.14: 35, + 2.14: 26.7, }, }, defaultBlowOutFlowRate: { - default: 57, + default: 26.7, valuesByApiLevel: { - 2.14: 57, + 2.14: 26.7, }, }, defaultDispenseFlowRate: { - default: 57, + default: 26.7, valuesByApiLevel: { - 2.14: 57, + 2.14: 26.7, }, }, defaultFlowAcceleration: 1200, diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 1b944418e0e2..aaef2eb24307 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,5 +1,5 @@ import type { CutoutFixtureId, CutoutId, AddressableAreaName } from '../deck' -import type { ModuleType } from './types' +import type { ModuleModel, ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) export const SLOT_LENGTH_MM = 127.76 // along X axis in robot coordinate system @@ -230,6 +230,16 @@ export const STAGING_AREA_CUTOUTS: CutoutId[] = [ 'cutoutD3', ] +export const TEMPERATURE_MODULE_CUTOUTS: CutoutId[] = [ + ...SINGLE_RIGHT_CUTOUTS, + ...SINGLE_LEFT_CUTOUTS, +] +export const HEATER_SHAKER_CUTOUTS: CutoutId[] = [ + ...SINGLE_RIGHT_CUTOUTS, + ...SINGLE_LEFT_CUTOUTS, +] +export const THERMOCYCLER_MODULE_CUTOUTS: CutoutId[] = ['cutoutA1', 'cutoutB1'] + export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' export const A1_ADDRESSABLE_AREA: 'A1' = 'A1' @@ -275,6 +285,98 @@ export const NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '96ChannelWasteChu export const GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA: 'gripperWasteChute' = 'gripperWasteChute' +export const THERMOCYCLER_ADDRESSABLE_AREA: 'thermocyclerModuleV2' = + 'thermocyclerModuleV2' +export const HEATERSHAKER_A1_ADDRESSABLE_AREA: 'heaterShakerV1A1' = + 'heaterShakerV1A1' +export const HEATERSHAKER_B1_ADDRESSABLE_AREA: 'heaterShakerV1B1' = + 'heaterShakerV1B1' +export const HEATERSHAKER_C1_ADDRESSABLE_AREA: 'heaterShakerV1C1' = + 'heaterShakerV1C1' +export const HEATERSHAKER_D1_ADDRESSABLE_AREA: 'heaterShakerV1D1' = + 'heaterShakerV1D1' +export const HEATERSHAKER_A3_ADDRESSABLE_AREA: 'heaterShakerV1A3' = + 'heaterShakerV1A3' +export const HEATERSHAKER_B3_ADDRESSABLE_AREA: 'heaterShakerV1B3' = + 'heaterShakerV1B3' +export const HEATERSHAKER_C3_ADDRESSABLE_AREA: 'heaterShakerV1C3' = + 'heaterShakerV1C3' +export const HEATERSHAKER_D3_ADDRESSABLE_AREA: 'heaterShakerV1D3' = + 'heaterShakerV1D3' +export const TEMPERATURE_MODULE_A1_ADDRESSABLE_AREA: 'temperatureModuleV2A1' = + 'temperatureModuleV2A1' +export const TEMPERATURE_MODULE_B1_ADDRESSABLE_AREA: 'temperatureModuleV2B1' = + 'temperatureModuleV2B1' +export const TEMPERATURE_MODULE_C1_ADDRESSABLE_AREA: 'temperatureModuleV2C1' = + 'temperatureModuleV2C1' +export const TEMPERATURE_MODULE_D1_ADDRESSABLE_AREA: 'temperatureModuleV2D1' = + 'temperatureModuleV2D1' +export const TEMPERATURE_MODULE_A3_ADDRESSABLE_AREA: 'temperatureModuleV2A3' = + 'temperatureModuleV2A3' +export const TEMPERATURE_MODULE_B3_ADDRESSABLE_AREA: 'temperatureModuleV2B3' = + 'temperatureModuleV2B3' +export const TEMPERATURE_MODULE_C3_ADDRESSABLE_AREA: 'temperatureModuleV2C3' = + 'temperatureModuleV2C3' +export const TEMPERATURE_MODULE_D3_ADDRESSABLE_AREA: 'temperatureModuleV2D3' = + 'temperatureModuleV2D3' + +export const MAGNETIC_BLOCK_A1_ADDRESSABLE_AREA: 'magneticBlockV1A1' = + 'magneticBlockV1A1' +export const MAGNETIC_BLOCK_B1_ADDRESSABLE_AREA: 'magneticBlockV1B1' = + 'magneticBlockV1B1' +export const MAGNETIC_BLOCK_C1_ADDRESSABLE_AREA: 'magneticBlockV1C1' = + 'magneticBlockV1C1' +export const MAGNETIC_BLOCK_D1_ADDRESSABLE_AREA: 'magneticBlockV1D1' = + 'magneticBlockV1D1' +export const MAGNETIC_BLOCK_A2_ADDRESSABLE_AREA: 'magneticBlockV1A2' = + 'magneticBlockV1A2' +export const MAGNETIC_BLOCK_B2_ADDRESSABLE_AREA: 'magneticBlockV1B2' = + 'magneticBlockV1B2' +export const MAGNETIC_BLOCK_C2_ADDRESSABLE_AREA: 'magneticBlockV1C2' = + 'magneticBlockV1C2' +export const MAGNETIC_BLOCK_D2_ADDRESSABLE_AREA: 'magneticBlockV1D2' = + 'magneticBlockV1D2' +export const MAGNETIC_BLOCK_A3_ADDRESSABLE_AREA: 'magneticBlockV1A3' = + 'magneticBlockV1A3' +export const MAGNETIC_BLOCK_B3_ADDRESSABLE_AREA: 'magneticBlockV1B3' = + 'magneticBlockV1B3' +export const MAGNETIC_BLOCK_C3_ADDRESSABLE_AREA: 'magneticBlockV1C3' = + 'magneticBlockV1C3' +export const MAGNETIC_BLOCK_D3_ADDRESSABLE_AREA: 'magneticBlockV1D3' = + 'magneticBlockV1D3' + +export const FLEX_MODULE_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + THERMOCYCLER_ADDRESSABLE_AREA, + HEATERSHAKER_A1_ADDRESSABLE_AREA, + HEATERSHAKER_B1_ADDRESSABLE_AREA, + HEATERSHAKER_C1_ADDRESSABLE_AREA, + HEATERSHAKER_D1_ADDRESSABLE_AREA, + HEATERSHAKER_A3_ADDRESSABLE_AREA, + HEATERSHAKER_B3_ADDRESSABLE_AREA, + HEATERSHAKER_C3_ADDRESSABLE_AREA, + HEATERSHAKER_D3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_A1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_B1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_C1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_D1_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_A3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_B3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_C3_ADDRESSABLE_AREA, + TEMPERATURE_MODULE_D3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D1_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D2_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_A3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_B3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_C3_ADDRESSABLE_AREA, + MAGNETIC_BLOCK_D3_ADDRESSABLE_AREA, +] + export const ADDRESSABLE_AREA_1: '1' = '1' export const ADDRESSABLE_AREA_2: '2' = '2' export const ADDRESSABLE_AREA_3: '3' = '3' @@ -359,6 +461,30 @@ export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: ' export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' = 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' +export const HEATERSHAKER_MODULE_V1_FIXTURE: 'heaterShakerModuleV1' = + 'heaterShakerModuleV1' +export const TEMPERATURE_MODULE_V2_FIXTURE: 'temperatureModuleV2' = + 'temperatureModuleV2' +export const MAGNETIC_BLOCK_V1_FIXTURE: 'magneticBlockV1' = 'magneticBlockV1' +export const STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE: 'stagingAreaSlotWithMagneticBlockV1' = + 'stagingAreaSlotWithMagneticBlockV1' +export const THERMOCYCLER_V2_REAR_FIXTURE: 'thermocyclerModuleV2Rear' = + 'thermocyclerModuleV2Rear' +export const THERMOCYCLER_V2_FRONT_FIXTURE: 'thermocyclerModuleV2Front' = + 'thermocyclerModuleV2Front' + +export const MODULE_FIXTURES_BY_MODEL: { + [moduleModel in ModuleModel]?: CutoutFixtureId[] +} = { + [HEATERSHAKER_MODULE_V1]: [HEATERSHAKER_MODULE_V1_FIXTURE], + [TEMPERATURE_MODULE_V2]: [TEMPERATURE_MODULE_V2_FIXTURE], + [MAGNETIC_BLOCK_V1]: [MAGNETIC_BLOCK_V1_FIXTURE], + [THERMOCYCLER_MODULE_V2]: [ + THERMOCYCLER_V2_REAR_FIXTURE, + THERMOCYCLER_V2_FRONT_FIXTURE, + ], +} + export const SINGLE_SLOT_FIXTURES: CutoutFixtureId[] = [ SINGLE_LEFT_SLOT_FIXTURE, SINGLE_CENTER_SLOT_FIXTURE, diff --git a/shared-data/js/deck/index.ts b/shared-data/js/deck/index.ts index 786325be5a7d..fce6ffb05fe5 100644 --- a/shared-data/js/deck/index.ts +++ b/shared-data/js/deck/index.ts @@ -1,5 +1,5 @@ -import flexDeckDefV4 from '../../deck/definitions/4/ot3_standard.json' -import ot2DeckDefV4 from '../../deck/definitions/4/ot2_standard.json' -import ot2DeckDefShortFixedTrashV4 from '../../deck/definitions/4/ot2_short_trash.json' +import flexDeckDefV5 from '../../deck/definitions/5/ot3_standard.json' +import ot2DeckDefV5 from '../../deck/definitions/5/ot2_standard.json' +import ot2DeckDefShortFixedTrashV5 from '../../deck/definitions/5/ot2_short_trash.json' -export { ot2DeckDefV4, ot2DeckDefShortFixedTrashV4, flexDeckDefV4 } +export { ot2DeckDefV5, ot2DeckDefShortFixedTrashV5, flexDeckDefV5 } diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index 7e2f117bca84..057bce01503d 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -6,9 +6,57 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + A1_ADDRESSABLE_AREA, + A2_ADDRESSABLE_AREA, + A3_ADDRESSABLE_AREA, + B1_ADDRESSABLE_AREA, + B2_ADDRESSABLE_AREA, + B3_ADDRESSABLE_AREA, + C1_ADDRESSABLE_AREA, + C2_ADDRESSABLE_AREA, + C3_ADDRESSABLE_AREA, + D1_ADDRESSABLE_AREA, + D2_ADDRESSABLE_AREA, + D3_ADDRESSABLE_AREA, + ADDRESSABLE_AREA_1, + ADDRESSABLE_AREA_2, + ADDRESSABLE_AREA_3, + ADDRESSABLE_AREA_4, + ADDRESSABLE_AREA_5, + ADDRESSABLE_AREA_6, + ADDRESSABLE_AREA_7, + ADDRESSABLE_AREA_8, + ADDRESSABLE_AREA_9, + ADDRESSABLE_AREA_10, + ADDRESSABLE_AREA_11, + HEATERSHAKER_MODULE_V1_FIXTURE, + HEATERSHAKER_MODULE_V1, + TEMPERATURE_MODULE_V2_FIXTURE, + TEMPERATURE_MODULE_V2, + MAGNETIC_BLOCK_V1_FIXTURE, + MAGNETIC_BLOCK_V1, + THERMOCYCLER_V2_REAR_FIXTURE, + THERMOCYCLER_MODULE_V2, + THERMOCYCLER_V2_FRONT_FIXTURE, + MODULE_FIXTURES_BY_MODEL, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, } from './constants' -import type { CutoutFixtureId, CutoutId, OT2CutoutId } from '../deck' -import type { AddressableArea, CoordinateTuple, DeckDefinition } from './types' +import { getModuleDisplayName } from './modules' +import { getCutoutIdForSlotName } from './helpers' +import type { + AddressableAreaName, + CutoutFixtureId, + CutoutId, + OT2CutoutId, +} from '../deck' +import type { + AddressableArea, + CoordinateTuple, + CutoutFixture, + DeckDefinition, + ModuleModel, +} from './types' +import type { ModuleLocation } from '../command' export function getCutoutDisplayName(cutout: CutoutId): string { return cutout.replace('cutout', '') @@ -107,66 +155,172 @@ export function getAddressableAreaFromSlotId( ) } +export function getCutoutFixtureIdsForModuleModel( + moduleModel: ModuleModel +): CutoutFixtureId[] { + const moduleFixtures = MODULE_FIXTURES_BY_MODEL[moduleModel] + return moduleFixtures ?? [] +} + +export function getCutoutFixturesForModuleModel( + moduleModel: ModuleModel, + deckDef: DeckDefinition +): CutoutFixture[] { + const moduleFixtureIds = getCutoutFixtureIdsForModuleModel(moduleModel) + return moduleFixtureIds.reduce((acc, id) => { + const moduleFixture = deckDef.cutoutFixtures.find(cf => cf.id === id) + return moduleFixture != null ? [...acc, moduleFixture] : acc + }, []) +} + +export function getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId: CutoutId | null, + moduleFixtures: CutoutFixture[] // cutout fixtures for a specific module model +): { [cutoutId in CutoutId]?: CutoutFixtureId } { + // find the first fixture for this specific module model that may mount to the cutout implied by the slotName + const anchorFixture = moduleFixtures.find(fixture => + fixture.mayMountTo.some(cutoutId => cutoutId === anchorCutoutId) + ) + if (anchorCutoutId != null && anchorFixture != null) { + const groupedFixtures = anchorFixture.fixtureGroup[anchorCutoutId] + return groupedFixtures?.[0] ?? { [anchorCutoutId]: anchorFixture.id } + } + return {} +} + +export function getFixtureIdByCutoutIdFromModuleSlotName( + slotName: string, + moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model + deckDef: DeckDefinition +): { [cutoutId in CutoutId]?: CutoutFixtureId } { + const anchorCutoutId = getCutoutIdForSlotName(slotName, deckDef) + return getFixtureIdByCutoutIdFromModuleAnchorCutoutId( + anchorCutoutId, + moduleFixtures + ) +} + +export function getCutoutIdsFromModuleSlotName( + slotName: string, + moduleFixtures: CutoutFixture[], // cutout fixtures for a specific module model + deckDef: DeckDefinition +): CutoutId[] { + const fixtureIdByCutoutId = getFixtureIdByCutoutIdFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + return Object.keys(fixtureIdByCutoutId) as CutoutId[] +} + +export function getAddressableAreaNamesFromLoadedModule( + moduleModel: ModuleModel, + slotName: ModuleLocation['slotName'], + deckDef: DeckDefinition +): AddressableAreaName[] { + const moduleFixtures = getCutoutFixturesForModuleModel(moduleModel, deckDef) + const cutoutIds = getCutoutIdsFromModuleSlotName( + slotName, + moduleFixtures, + deckDef + ) + return moduleFixtures.reduce((acc, cutoutFixture) => { + const providedAddressableAreas = cutoutIds.reduce( + (innerAcc, cutoutId) => { + const newAddressableAreas = + cutoutFixture?.providesAddressableAreas[cutoutId] ?? [] + return [...innerAcc, ...newAddressableAreas] + }, + [] + ) + return [...acc, ...providedAddressableAreas] + }, []) +} + export function getFixtureDisplayName( - cutoutFixtureId: CutoutFixtureId | null + cutoutFixtureId: CutoutFixtureId | null, + usbPortNumber?: number ): string { - if (cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE) { - return 'Staging area slot' - } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { - return 'Trash bin' - } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE) { - return 'Waste chute only' - } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE) { - return 'Waste chute only with cover' - } else if ( - cutoutFixtureId === - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE - ) { - return 'Waste chute with staging area slot' - } else if ( - cutoutFixtureId === - STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE - ) { - return 'Waste chute with staging area slot and cover' - } else { - return 'Slot' + switch (cutoutFixtureId) { + case STAGING_AREA_RIGHT_SLOT_FIXTURE: + return 'Staging area slot' + case TRASH_BIN_ADAPTER_FIXTURE: + return 'Trash bin' + case WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: + return 'Waste chute only' + case WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: + return 'Waste chute only with cover' + case STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: + return 'Waste chute with staging area slot' + case STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: + return 'Waste chute with staging area slot and cover' + case HEATERSHAKER_MODULE_V1_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + HEATERSHAKER_MODULE_V1 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(HEATERSHAKER_MODULE_V1) + case TEMPERATURE_MODULE_V2_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + TEMPERATURE_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(TEMPERATURE_MODULE_V2) + case MAGNETIC_BLOCK_V1_FIXTURE: + return `${getModuleDisplayName(MAGNETIC_BLOCK_V1)}` + case STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE: + return `${getModuleDisplayName(MAGNETIC_BLOCK_V1)} with staging area slot` + case THERMOCYCLER_V2_REAR_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + THERMOCYCLER_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(THERMOCYCLER_MODULE_V2) + case THERMOCYCLER_V2_FRONT_FIXTURE: + return usbPortNumber != null + ? `${getModuleDisplayName( + THERMOCYCLER_MODULE_V2 + )} in USB-${usbPortNumber}` + : getModuleDisplayName(THERMOCYCLER_MODULE_V2) + default: + return 'Slot' } } -const STANDARD_OT2_SLOTS = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', +const STANDARD_OT2_SLOTS: AddressableAreaName[] = [ + ADDRESSABLE_AREA_1, + ADDRESSABLE_AREA_2, + ADDRESSABLE_AREA_3, + ADDRESSABLE_AREA_4, + ADDRESSABLE_AREA_5, + ADDRESSABLE_AREA_6, + ADDRESSABLE_AREA_7, + ADDRESSABLE_AREA_8, + ADDRESSABLE_AREA_9, + ADDRESSABLE_AREA_10, + ADDRESSABLE_AREA_11, ] -const STANDARD_FLEX_SLOTS = [ - 'A1', - 'A2', - 'A3', - 'B1', - 'B2', - 'B3', - 'C1', - 'C2', - 'C3', - 'D1', - 'D2', - 'D3', +const STANDARD_FLEX_SLOTS: AddressableAreaName[] = [ + A1_ADDRESSABLE_AREA, + A2_ADDRESSABLE_AREA, + A3_ADDRESSABLE_AREA, + B1_ADDRESSABLE_AREA, + B2_ADDRESSABLE_AREA, + B3_ADDRESSABLE_AREA, + C1_ADDRESSABLE_AREA, + C2_ADDRESSABLE_AREA, + C3_ADDRESSABLE_AREA, + D1_ADDRESSABLE_AREA, + D2_ADDRESSABLE_AREA, + D3_ADDRESSABLE_AREA, ] export const isAddressableAreaStandardSlot = ( - addressableAreaId: string, + addressableAreaName: AddressableAreaName, deckDef: DeckDefinition ): boolean => (deckDef.robot.model === FLEX_ROBOT_TYPE ? STANDARD_FLEX_SLOTS : STANDARD_OT2_SLOTS - ).includes(addressableAreaId) + ).includes(addressableAreaName) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts new file mode 100644 index 000000000000..d83239e3ec9c --- /dev/null +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterDefaultValue.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, vi } from 'vitest' +import { formatRunTimeParameterDefaultValue } from '../formatRunTimeParameterDefaultValue' + +import type { RunTimeParameter } from '../../types' + +const capitalizeFirstLetter = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) + +describe('formatRunTimeParameterDefaultValue', () => { + it('should return value with suffix when type is int', () => { + const mockData = { + value: 6, + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + suffix: 'samples', + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('6 samples') + }) + + it('should return value with suffix when type is float', () => { + const mockData = { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('6.5 mL') + }) + + it('should return value when type is str', () => { + const mockData = { + value: 'left', + displayName: 'pipette mount', + variableName: 'mount', + description: 'pipette mount', + type: 'str', + choices: [ + { + displayName: 'Left', + value: 'left', + }, + { + displayName: 'Right', + value: 'right', + }, + ], + default: 'left', + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('Left') + }) + + it('should return value when type is int choice with suffix', () => { + const mockData = { + value: 5, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'int', + suffix: 'mL', + min: 1, + max: 10, + choices: [ + { + displayName: 'one', + value: 1, + }, + { + displayName: 'six', + value: 6, + }, + ], + default: 5, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is float choice with suffix', () => { + const mockData = { + value: 5.0, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'float', + suffix: 'mL', + min: 1.0, + max: 10.0, + choices: [ + { + displayName: 'one', + value: 1.0, + }, + { + displayName: 'six', + value: 6.0, + }, + ], + default: 5.0, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is boolean true', () => { + const mockData = { + value: true, + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature on the module', + type: 'bool', + default: true, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('On') + }) + + it('should return value when type is boolean false', () => { + const mockData = { + value: false, + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'bool', + default: false, + } as RunTimeParameter + const result = formatRunTimeParameterDefaultValue(mockData, mockTFunction) + expect(result).toEqual('Off') + }) +}) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx new file mode 100644 index 000000000000..07190fac23ea --- /dev/null +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterMinMax.test.tsx @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest' +import { formatRunTimeParameterMinMax } from '../formatRunTimeParameterMinMax' + +import type { RunTimeParameter } from '../../types' + +describe('utils-formatRunTimeParameterMinMax', () => { + it('should return int min and max', () => { + const mockData = { + value: 6, + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1-10') + }) + + it('should return value with suffix when type is float', () => { + const mockData = { + value: 6.5, + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + } as RunTimeParameter + const result = formatRunTimeParameterMinMax(mockData) + expect(result).toEqual('1.5-10.0') + }) +}) diff --git a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts index bfdad4939137..8e228cb6dbc1 100644 --- a/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts +++ b/shared-data/js/helpers/__tests__/formatRunTimeParameterValue.test.ts @@ -9,7 +9,7 @@ const capitalizeFirstLetter = (str: string): string => { const mockTFunction = vi.fn(str => capitalizeFirstLetter(str)) -describe('utils-formatRunTimeParameterValue', () => { +describe('utils-formatRunTimeParameterDefaultValue', () => { it('should return value with suffix when type is int', () => { const mockData = { value: 6, @@ -41,11 +41,11 @@ describe('utils-formatRunTimeParameterValue', () => { expect(result).toEqual('6.5 mL') }) - it('should return value with suffix when type is str', () => { + it('should return value when type is str', () => { const mockData = { value: 'left', displayName: 'pipette mount', - variableName: 'mont', + variableName: 'mount', description: 'pipette mount', type: 'str', choices: [ @@ -64,26 +64,78 @@ describe('utils-formatRunTimeParameterValue', () => { expect(result).toEqual('Left') }) - it('should return value with suffix when type is boolean true', () => { + it('should return value when type is int choice with suffix', () => { + const mockData = { + value: 5, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'int', + suffix: 'mL', + min: 1, + max: 10, + choices: [ + { + displayName: 'one', + value: 1, + }, + { + displayName: 'six', + value: 6, + }, + ], + default: 5, + } as RunTimeParameter + const result = formatRunTimeParameterValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is float choice with suffix', () => { + const mockData = { + value: 5.0, + displayName: 'num', + variableName: 'number', + description: 'its just number', + type: 'float', + suffix: 'mL', + min: 1.0, + max: 10.0, + choices: [ + { + displayName: 'one', + value: 1.0, + }, + { + displayName: 'six', + value: 6.0, + }, + ], + default: 5.0, + } as RunTimeParameter + const result = formatRunTimeParameterValue(mockData, mockTFunction) + expect(result).toEqual('5 mL') + }) + + it('should return value when type is boolean true', () => { const mockData = { value: true, displayName: 'Deactivate Temperatures', variableName: 'DEACTIVATE_TEMP', description: 'deactivate temperature on the module', - type: 'boolean', + type: 'bool', default: true, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) expect(result).toEqual('On') }) - it('should return value with suffix when type is boolean false', () => { + it('should return value when type is boolean false', () => { const mockData = { value: false, displayName: 'Dry Run', variableName: 'DRYRUN', description: 'Is this a dry or wet run? Wet is true, dry is false', - type: 'boolean', + type: 'bool', default: false, } as RunTimeParameter const result = formatRunTimeParameterValue(mockData, mockTFunction) diff --git a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts index 9c7a1318e06d..8e34261756b0 100644 --- a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts +++ b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' -import ot2DeckDef from '../../../deck/definitions/4/ot2_standard.json' -import ot3DeckDef from '../../../deck/definitions/4/ot3_standard.json' +import ot2DeckDef from '../../../deck/definitions/5/ot2_standard.json' +import ot3DeckDef from '../../../deck/definitions/5/ot3_standard.json' import { getDeckDefFromRobotType } from '..' describe('getDeckDefFromRobotType', () => { diff --git a/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts new file mode 100644 index 000000000000..2a5b62b265d4 --- /dev/null +++ b/shared-data/js/helpers/__tests__/orderRuntimeParameterRangeOptions.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' + +import { + isNumeric, + orderRuntimeParameterRangeOptions, +} from '../orderRuntimeParameterRangeOptions' + +import type { Choice } from '../../types' + +describe('isNumeric', () => { + it('should return true when input is "2"', () => { + const result = isNumeric('2') + expect(result).toBeTruthy() + }) + + it('should return false when input is "opentrons"', () => { + const result = isNumeric('opentrons') + expect(result).toBeFalsy() + }) +}) + +describe('orderRuntimeParameterRangeOptions', () => { + it('should return numerical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('16, 20') + }) + + it('should return alphabetical order when choices are number', () => { + const mockChoices: Choice[] = [ + { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('Eight Channel 50µL, Single channel 50µL') + }) + + it('should return empty string choices > 3', () => { + const mockChoices: Choice[] = [ + { displayName: '20', value: 20 }, + { displayName: '16', value: 16 }, + { displayName: '18', value: 18 }, + ] + const result = orderRuntimeParameterRangeOptions(mockChoices) + expect(result).toEqual('') + }) +}) diff --git a/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts new file mode 100644 index 000000000000..3ac5cda5bfa7 --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterDefaultValue.ts @@ -0,0 +1,50 @@ +import type { RunTimeParameter } from '../types' + +/** + * Formats the runtime parameter's default value. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter whose default value is to be formatted. + * @param {Function} [t] - An optional function for localization. + * + * @returns {string} The formatted default value of the runtime parameter. + * + */ + +export const formatRunTimeParameterDefaultValue = ( + runTimeParameter: RunTimeParameter, + t?: any +): string => { + const { type, default: defaultValue } = runTimeParameter + const suffix = + 'suffix' in runTimeParameter && runTimeParameter.suffix != null + ? runTimeParameter.suffix + : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } + + switch (type) { + case 'int': + case 'float': + return suffix !== null + ? `${defaultValue.toString()} ${suffix}` + : defaultValue.toString() + case 'bool': + if (t != null) { + return Boolean(defaultValue) ? t('on') : t('off') + } else { + return Boolean(defaultValue) ? 'On' : 'Off' + } + default: + break + } + return '' +} diff --git a/shared-data/js/helpers/formatRunTimeParameterMinMax.ts b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts new file mode 100644 index 000000000000..632dec5c020c --- /dev/null +++ b/shared-data/js/helpers/formatRunTimeParameterMinMax.ts @@ -0,0 +1,32 @@ +import type { RunTimeParameter } from '../types' +/** + * Formats the runtime parameter's minimum and maximum values. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter whose min and max values are to be formatted. + * + * @returns {string} The formatted min-max value of the runtime parameter. + * + * @example + * const runTimeParameter = { + * value: 6.5, + * displayName: 'EtoH Volume', + * variableName: 'ETOH_VOLUME', + * description: '70% ethanol volume', + * type: 'float', + * suffix: 'mL', + * min: 1.5, + * max: 10.0, + * default: 6.5, + * } + * console.log(formatRunTimeParameterMinMax(runTimeParameter)); // "1.5-10.0" + */ + +export const formatRunTimeParameterMinMax = ( + runTimeParameter: RunTimeParameter +): string => { + const min = 'min' in runTimeParameter ? runTimeParameter.min : 0 + const max = 'max' in runTimeParameter ? runTimeParameter.max : 0 + return runTimeParameter.type === 'int' + ? `${min}-${max}` + : `${min.toFixed(1)}-${max.toFixed(1)}` +} diff --git a/shared-data/js/helpers/formatRunTimeParameterValue.ts b/shared-data/js/helpers/formatRunTimeParameterValue.ts index ffbab087849f..a6a3ad4d7ecd 100644 --- a/shared-data/js/helpers/formatRunTimeParameterValue.ts +++ b/shared-data/js/helpers/formatRunTimeParameterValue.ts @@ -1,35 +1,45 @@ -import { RunTimeParameter } from '../types' +import type { RunTimeParameter } from '../types' + +/** + * Formats the runtime parameter value. + * + * @param {RunTimeParameter} runTimeParameter - The runtime parameter to be formatted. + * @param {Function} t - A function for localization. + * + * @returns {string} The formatted runtime parameter value. + * + */ export const formatRunTimeParameterValue = ( runTimeParameter: RunTimeParameter, - t?: any + t: any ): string => { - const { type, default: defaultValue } = runTimeParameter + const { type, value } = runTimeParameter const suffix = 'suffix' in runTimeParameter && runTimeParameter.suffix != null ? runTimeParameter.suffix : null + + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === value + ) + if (choice != null) { + return suffix != null + ? `${choice.displayName} ${suffix}` + : choice.displayName + } + } switch (type) { case 'int': case 'float': return suffix !== null - ? `${defaultValue.toString()} ${suffix}` - : defaultValue.toString() - case 'boolean': - if (t != null) { - return Boolean(defaultValue) ? t('on') : t('off') - } else { - return Boolean(defaultValue) ? 'On' : 'Off' - } - case 'str': - if ('choices' in runTimeParameter && runTimeParameter.choices != null) { - const choice = runTimeParameter.choices.find( - choice => choice.value === defaultValue - ) - if (choice != null) { - return choice.displayName - } - } + ? `${value.toString()} ${suffix}` + : value.toString() + case 'bool': { + return Boolean(value) ? t('on') : t('off') + } + default: break } return '' diff --git a/shared-data/js/helpers/getAddressableAreasInProtocol.ts b/shared-data/js/helpers/getAddressableAreasInProtocol.ts index 1ca3013930a7..81222777db22 100644 --- a/shared-data/js/helpers/getAddressableAreasInProtocol.ts +++ b/shared-data/js/helpers/getAddressableAreasInProtocol.ts @@ -1,5 +1,8 @@ import { MOVABLE_TRASH_A3_ADDRESSABLE_AREA } from '../constants' -import { getAddressableAreaFromSlotId } from '../fixtures' +import { + getAddressableAreaNamesFromLoadedModule, + getAddressableAreaFromSlotId, +} from '../fixtures' import type { AddressableAreaName } from '../../deck' import type { ProtocolAnalysisOutput } from '../../protocol' import type { CompletedProtocolAnalysis, DeckDefinition } from '../types' @@ -12,16 +15,15 @@ export function getAddressableAreasInProtocol( const addressableAreasFromCommands = commands.reduce( (acc, command) => { + const { commandType, params } = command if ( - command.commandType === 'moveLabware' && - command.params.newLocation !== 'offDeck' && - 'slotName' in command.params.newLocation && - !acc.includes( - command.params.newLocation.slotName as AddressableAreaName - ) + commandType === 'moveLabware' && + params.newLocation !== 'offDeck' && + 'slotName' in params.newLocation && + !acc.includes(params.newLocation.slotName as AddressableAreaName) ) { const addressableAreaName = getAddressableAreaFromSlotId( - command.params.newLocation.slotName, + params.newLocation.slotName, deckDef )?.id @@ -31,51 +33,61 @@ export function getAddressableAreasInProtocol( return [...acc, addressableAreaName] } } else if ( - command.commandType === 'moveLabware' && - command.params.newLocation !== 'offDeck' && - 'addressableAreaName' in command.params.newLocation && - !acc.includes(command.params.newLocation.addressableAreaName) + commandType === 'moveLabware' && + params.newLocation !== 'offDeck' && + 'addressableAreaName' in params.newLocation && + !acc.includes(params.newLocation.addressableAreaName) ) { - return [...acc, command.params.newLocation.addressableAreaName] + return [...acc, params.newLocation.addressableAreaName] } else if ( - (command.commandType === 'loadLabware' || - command.commandType === 'loadModule') && - command.params.location !== 'offDeck' && - 'slotName' in command.params.location && - !acc.includes(command.params.location.slotName as AddressableAreaName) + commandType === 'loadLabware' && + params.location !== 'offDeck' && + 'slotName' in params.location && + !acc.includes(params.location.slotName as AddressableAreaName) ) { const addressableAreaName = getAddressableAreaFromSlotId( - command.params.location.slotName, + params.location.slotName, deckDef )?.id // do not add addressable area name for legacy trash labware if ( addressableAreaName == null || - ('loadName' in command.params && - command.params.loadName === 'opentrons_1_trash_3200ml_fixed') + ('loadName' in params && + params.loadName === 'opentrons_1_trash_3200ml_fixed') ) { return acc } else { return [...acc, addressableAreaName] } } else if ( - command.commandType === 'loadLabware' && - command.params.location !== 'offDeck' && - 'addressableAreaName' in command.params.location && - !acc.includes(command.params.location.addressableAreaName) + commandType === 'loadModule' && + !acc.includes(params.location.slotName as AddressableAreaName) + ) { + const addressableAreaNames = getAddressableAreaNamesFromLoadedModule( + params.model, + params.location.slotName, + deckDef + ) + + return [...acc, ...addressableAreaNames] + } else if ( + commandType === 'loadLabware' && + params.location !== 'offDeck' && + 'addressableAreaName' in params.location && + !acc.includes(params.location.addressableAreaName) ) { - return [...acc, command.params.location.addressableAreaName] + return [...acc, params.location.addressableAreaName] } else if ( - command.commandType === 'moveToAddressableArea' && - !acc.includes(command.params.addressableAreaName) + commandType === 'moveToAddressableArea' && + !acc.includes(params.addressableAreaName) ) { - return [...acc, command.params.addressableAreaName] + return [...acc, params.addressableAreaName] } else if ( - command.commandType === 'moveToAddressableAreaForDropTip' && - !acc.includes(command.params.addressableAreaName) + commandType === 'moveToAddressableAreaForDropTip' && + !acc.includes(params.addressableAreaName) ) { - return [...acc, command.params.addressableAreaName] + return [...acc, params.addressableAreaName] } else { return acc } diff --git a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts index e4017199156b..65c4ccac3e5b 100644 --- a/shared-data/js/helpers/getSimplestFlexDeckConfig.ts +++ b/shared-data/js/helpers/getSimplestFlexDeckConfig.ts @@ -2,7 +2,7 @@ import { FLEX_ROBOT_TYPE } from '../constants' import { getAddressableAreaFromSlotId } from '../fixtures' import { getAddressableAreasInProtocol, getDeckDefFromRobotType } from '.' -import type { AddressableAreaName, CutoutId } from '../../deck' +import type { AddressableAreaName, CutoutFixtureId, CutoutId } from '../../deck' import type { ProtocolAnalysisOutput } from '../../protocol' import type { CutoutConfig, @@ -10,6 +10,7 @@ import type { DeckDefinition, DeckConfiguration, CompletedProtocolAnalysis, + CutoutFixtureGroup, } from '../types' export interface CutoutConfigProtocolSpec extends CutoutConfig { @@ -111,7 +112,6 @@ export function getSimplestDeckConfigForProtocol( } return acc }, FLEX_SIMPLEST_DECK_CONFIG_PROTOCOL_SPEC) - return simplestDeckConfig } @@ -151,6 +151,15 @@ export function getCutoutIdForSlotName( return cutoutIdForSlotName } +export function getFixtureGroupForCutoutFixture( + cutoutFixtureId: CutoutFixtureId, + cutoutFixtures: CutoutFixture[] +): CutoutFixtureGroup { + return ( + cutoutFixtures.find(cf => cf.id === cutoutFixtureId)?.fixtureGroup ?? {} + ) +} + export function getCutoutIdForAddressableArea( addressableArea: AddressableAreaName, cutoutFixtures: CutoutFixture[] diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 2d78f16ca1f0..a07d10472f65 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -1,8 +1,8 @@ import uniq from 'lodash/uniq' import { OPENTRONS_LABWARE_NAMESPACE } from '../constants' -import standardOt2DeckDef from '../../deck/definitions/4/ot2_standard.json' -import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json' +import standardOt2DeckDef from '../../deck/definitions/5/ot2_standard.json' +import standardFlexDeckDef from '../../deck/definitions/5/ot3_standard.json' import type { DeckDefinition, LabwareDefinition2, @@ -28,7 +28,10 @@ export * from './getOccludedSlotCountForModule' export * from './labwareInference' export * from './getAddressableAreasInProtocol' export * from './getSimplestFlexDeckConfig' +export * from './formatRunTimeParameterDefaultValue' export * from './formatRunTimeParameterValue' +export * from './formatRunTimeParameterMinMax' +export * from './orderRuntimeParameterRangeOptions' export const getLabwareDefIsStandard = (def: LabwareDefinition2): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE @@ -201,6 +204,23 @@ export const getWellsDepth = ( return offsets[0] } +export const getWellDimension = ( + labwareDef: LabwareDefinition2, + wells: string[], + position: 'x' | 'y' +): number => { + const offsets = wells.map(well => { + const labwareWell = labwareDef.wells[well] + const shape = labwareWell.shape + if (shape === 'circular') { + return labwareWell.diameter + } else { + return position === 'x' ? labwareWell.xDimension : labwareWell.yDimension + } + }) + return offsets[0] +} + export const getSlotHasMatingSurfaceUnitVector = ( deckDef: DeckDefinition, addressableAreaName: string diff --git a/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts new file mode 100644 index 000000000000..826fc958dd17 --- /dev/null +++ b/shared-data/js/helpers/orderRuntimeParameterRangeOptions.ts @@ -0,0 +1,46 @@ +import type { Choice } from '../types' + +export const isNumeric = (str: string): boolean => { + return !isNaN(Number(str)) +} + +/** + * This function sorts an array of strings in numerical and alphabetical order. + * @param {Choice[]} - The array of Choice + * Choice is an object like {displayName: 'Single channel 50µL', value: 'flex_1channel_50' } + * @returns {string} The ordered string with "," + * + * @example + * const numChoices = [ + * { displayName: '20', value: 20 }, + * { displayName: '16', value: 16 }, + * ] + * console.log(orderRuntimeParameterRangeOptions(numChoices) // 16,20 + * + * const strChoices = [ + * { displayName: 'Single channel 50µL', value: 'flex_1channel_50' }, + * { displayName: 'Eight Channel 50µL', value: 'flex_8channel_50' }, + * ] + * console.log(orderRuntimeParameterRangeOptions(strChoices) // Eight Channel 50µL, Single channel 50µL + */ +export const orderRuntimeParameterRangeOptions = ( + choices: Choice[] +): string => { + // when this function is called, the array length is always 2 + if (choices.length > 2) { + console.error(`expected to have length 2 but has length ${choices.length}`) + return '' + } + const displayNames = [choices[0].displayName, choices[1].displayName] + if (isNumeric(displayNames[0])) { + return displayNames + .sort((a, b) => { + const numA = Number(a) + const numB = Number(b) + return numA - numB + }) + .join(', ') + } else { + return displayNames.sort().join(', ') + } +} diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 12e991ce7f3d..4d51f992f22e 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -270,11 +270,17 @@ export interface DeckCalibrationPoint { displayName: string } +export type CutoutFixtureGroup = { + [cutoutId in CutoutId]?: Array<{ [cutoutId in CutoutId]?: CutoutFixtureId }> +} + export interface CutoutFixture { id: CutoutFixtureId mayMountTo: CutoutId[] displayName: string providesAddressableAreas: Record + expectOpentronsModuleSerialNumber: boolean + fixtureGroup: CutoutFixtureGroup height: number } @@ -486,6 +492,7 @@ export interface SupportedTip { } defaultReturnTipHeight?: number defaultFlowAcceleration?: number + uiMaxFlowRate?: number } export interface SupportedTips { @@ -590,38 +597,37 @@ export interface AnalysisError { createdAt: string } -export interface NumberParameter { +export interface NumberParameter extends BaseRunTimeParameter { type: NumberParameterType min: number max: number default: number } -interface Choice { +export interface Choice { displayName: string value: number | boolean | string } -interface ChoiceParameter { +interface ChoiceParameter extends BaseRunTimeParameter { type: RunTimeParameterType choices: Choice[] default: number | boolean | string } -interface BooleanParameter { +interface BooleanParameter extends BaseRunTimeParameter { type: BooleanParameterType default: boolean } type NumberParameterType = 'int' | 'float' -type BooleanParameterType = 'boolean' +type BooleanParameterType = 'bool' type StringParameterType = 'str' type RunTimeParameterType = | NumberParameter | BooleanParameterType | StringParameterType -type ParameterType = NumberParameter | ChoiceParameter | BooleanParameter interface BaseRunTimeParameter { displayName: string variableName: string @@ -630,7 +636,10 @@ interface BaseRunTimeParameter { suffix?: string } -export type RunTimeParameter = BaseRunTimeParameter & ParameterType +export type RunTimeParameter = + | BooleanParameter + | ChoiceParameter + | NumberParameter // TODO(BC, 10/25/2023): this type (and others in this file) probably belong in api-client, not here export interface CompletedProtocolAnalysis { @@ -720,7 +729,8 @@ export type StatusBarAnimations = StatusBarAnimation[] export interface CutoutConfig { cutoutId: CutoutId - cutoutFixtureId: CutoutFixtureId | null + cutoutFixtureId: CutoutFixtureId + opentronsModuleSerialNumber?: string } export type DeckConfiguration = CutoutConfig[] diff --git a/shared-data/pipette/definitions/1/pipetteModelSpecs.json b/shared-data/pipette/definitions/1/pipetteModelSpecs.json index 7a039c0e33f5..c6367e851b44 100644 --- a/shared-data/pipette/definitions/1/pipetteModelSpecs.json +++ b/shared-data/pipette/definitions/1/pipetteModelSpecs.json @@ -10304,6 +10304,175 @@ "quirks": [], "returnTipHeight": 0.83, "idleCurrent": 0.3 + }, + "p1000_96_v3.6": { + "name": "p1000_96", + "backCompatNames": [], + "top": { + "value": 0.5, + "min": 0, + "max": 45, + "units": "mm", + "type": "float" + }, + "bottom": { + "value": 71.5, + "min": 55, + "max": 80, + "type": "float", + "units": "mm" + }, + "blowout": { + "value": 76.5, + "min": 60, + "max": 85, + "units": "mm", + "type": "float" + }, + "dropTip": { + "value": 92.5, + "min": 78, + "max": 119, + "units": "mm", + "type": "float" + }, + "pickUpCurrent": { + "value": 0.5, + "min": 0.05, + "max": 2.0, + "units": "amps", + "type": "float" + }, + "pickUpDistance": { + "value": 13, + "min": 1, + "max": 30, + "units": "mm", + "type": "float" + }, + "pickUpIncrement": { + "value": 0.0, + "min": 0.0, + "max": 10.0, + "units": "mm", + "type": "float" + }, + "pickUpPresses": { + "value": 1, + "min": 0, + "max": 10, + "units": "presses", + "type": "int" + }, + "pickUpSpeed": { + "value": 10, + "min": 1, + "max": 30, + "units": "mm/s", + "type": "float" + }, + "nozzleOffset": [-8.0, -16.0, -259.15], + "modelOffset": [0.0, 0.0, 25.14], + "ulPerMm": [ + { + "aspirate": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ], + + "dispense": [ + [0.6464, 0.4817, 0.0427], + [1.0889, 0.2539, 0.1591], + [1.5136, 0.1624, 0.2587], + [1.9108, 0.1042, 0.3467], + [2.2941, 0.0719, 0.4085], + [2.9978, 0.037, 0.4886], + [3.7731, 0.0378, 0.4863], + [4.7575, 0.0516, 0.4342], + [5.5024, 0.011, 0.6275], + [6.2686, 0.0114, 0.6253], + [7.005, 0.0054, 0.6625], + [8.5207, 0.0063, 0.6563], + [10.0034, 0.003, 0.6844], + [11.5075, 0.0031, 0.6833], + [13.0327, 0.0032, 0.6829], + [14.5356, 0.0018, 0.7003], + [17.5447, 0.0014, 0.7063], + [20.5576, 0.0011, 0.7126], + [23.5624, 0.0007, 0.7197], + [26.5785, 0.0007, 0.721], + [29.593, 0.0005, 0.7248], + [32.6109, 0.0004, 0.7268], + [35.6384, 0.0004, 0.727], + [38.6439, 0.0002, 0.7343], + [41.6815, 0.0004, 0.7284], + [44.6895, 0.0002, 0.7372], + [47.6926, 0.0001, 0.7393], + [51.4567, 0.0001, 0.7382] + ] + } + ], + "plungerCurrent": { + "value": 1, + "min": 0.1, + "max": 1.5, + "units": "amps", + "type": "float" + }, + "dropTipCurrent": { + "value": 1, + "min": 0.1, + "max": 1.25, + "units": "amps", + "type": "float" + }, + "dropTipSpeed": { + "value": 10, + "min": 0.001, + "max": 30, + "units": "mm/sec", + "type": "float" + }, + "tipOverlap": { + "default": 10.5, + "opentrons/opentrons_96_tiprack_50ul/1": 10.5 + }, + "tipLength": { + "value": 78.3, + "units": "mm", + "type": "float", + "min": 0, + "max": 100 + }, + "quirks": [], + "returnTipHeight": 0.83, + "idleCurrent": 0.3 } }, "mutableConfigs": [ diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json new file mode 100644 index 000000000000..c59dfce42ab6 --- /dev/null +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -0,0 +1,114 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", + "displayName": "Flex 96-Channel 1000 μL", + "model": "p1000", + "displayCategory": "FLEX", + "pickUpTipConfigurations": { + "pressFit": { + "presses": 1, + "increment": 0.0, + "speedByTipCount": { + "1": 10.0, + "2": 10.0, + "3": 10.0, + "4": 10.0, + "5": 10.0, + "6": 10.0, + "7": 10.0, + "8": 10.0, + "12": 10.0, + "16": 10.0, + "24": 10.0, + "48": 10.0 + }, + "distanceByTipCount": { + "1": 13.0, + "2": 13.0, + "3": 13.0, + "4": 13.0, + "5": 13.0, + "6": 13.0, + "7": 13.0, + "8": 13.0, + "12": 13.0, + "16": 13.0, + "24": 13.0, + "48": 13.0 + }, + + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 8.25, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } + }, + "dropTipConfigurations": { + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } + }, + "plungerMotorConfigurations": { + "idle": 0.3, + "run": 0.8 + }, + "plungerPositionsConfigurations": { + "default": { + "top": 0.5, + "bottom": 68.5, + "blowout": 73.5, + "drop": 80 + } + }, + "availableSensors": { + "sensors": ["pressure", "capacitive", "environment"], + "pressure": { + "count": 2 + }, + "capacitive": { + "count": 2 + }, + "environment": { + "count": 1 + } + }, + "partialTipConfigurations": { + "partialTipSupported": true, + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] + }, + "backCompatNames": [], + "channels": 96, + "shaftDiameter": 4.5, + "shaftULperMM": 15.904, + "backlashDistance": 3.0, + "quirks": [], + "plungerHomingConfigurations": { + "current": 0.8, + "speed": 5 + }, + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 +} diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json new file mode 100644 index 000000000000..da209a72907c --- /dev/null +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_6.json @@ -0,0 +1,295 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", + "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", + "nozzleOffset": [-36.0, -25.5, -259.15], + "pipetteBoundingBoxOffsets": { + "backLeftCorner": [-67.0, -3.5, -259.15], + "frontRightCorner": [94.0, -113.0, -259.15] + }, + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], + "nozzleMap": { + "A1": [-36.0, -25.5, -259.15], + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] + } +} diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json index 12736030d8e8..fd4f29a83bbd 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json index ae95738fb09c..dcc9d5334908 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json index 1906adc83725..83026842153e 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 802.9, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -82,6 +83,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 847.9, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -160,6 +162,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 744.6, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json index 22950b768756..aa83a2e5bda8 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p20/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 7.6, "valuesByApiLevel": { "2.0": 7.6 } @@ -86,6 +87,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 23, "defaultAspirateFlowRate": { "default": 7.6, "valuesByApiLevel": { "2.0": 7.6 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json index a7d91165db79..4fee623f602d 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p300/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t200": { + "uiMaxFlowRate": 335.3, "defaultAspirateFlowRate": { "default": 94, "valuesByApiLevel": { "2.0": 94 } @@ -89,6 +90,7 @@ "defaultPushOutVolume": 0 }, "t300": { + "uiMaxFlowRate": 335.3, "defaultAspirateFlowRate": { "default": 94, "valuesByApiLevel": { "2.0": 94 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json index ac12e0bea1e5..38a4b01df80b 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json index 352f61bae303..32131ee19820 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json index 49b2a7b549de..ca2a48db2744 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.7, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json index 4e83eee5d817..cc629f28316d 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json index 881e9583aa51..0e9284b04e6c 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_4.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json index 881e9583aa51..0e9284b04e6c 100644 --- a/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/eight_channel/p50/lowVolumeDefault/3_5.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 32.6, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 32.6, + "valuesByApiLevel": { "2.14": 32.6 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json index 0f3f56f64942..899d08aeaee4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 184.8, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json index 0f3f56f64942..899d08aeaee4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 184.8, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json index 0f3f56f64942..1b9c88edf92e 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 189.1, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -56,6 +57,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -108,6 +110,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 185.1, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json new file mode 100644 index 000000000000..cd27abd81f0e --- /dev/null +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_6.json @@ -0,0 +1,191 @@ +{ + "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", + "supportedTips": { + "t50": { + "uiMaxFlowRate": 194, + "defaultAspirateFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultDispenseFlowRate": { + "default": 6, + "valuesByApiLevel": { "2.14": 6 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 57.9, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [1.9733, 2.7039, 5.1258], + [2.88, 1.0915, 8.3077], + [3.7642, 0.5906, 9.7502], + [4.9783, 1.0072, 8.1822], + [5.9342, 0.2998, 11.7038], + [6.8708, 0.1887, 12.3626], + [7.8092, 0.1497, 12.631], + [8.7525, 0.1275, 12.804], + [13.4575, 0.0741, 13.2718], + [22.8675, 0.0296, 13.87], + [37.0442, 0.0128, 14.2551], + [55.4792, -0.0013, 14.7754] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.9733, 2.7039, 5.1258], + [2.88, 1.0915, 8.3077], + [3.7642, 0.5906, 9.7502], + [4.9783, 1.0072, 8.1822], + [5.9342, 0.2998, 11.7038], + [6.8708, 0.1887, 12.3626], + [7.8092, 0.1497, 12.631], + [8.7525, 0.1275, 12.804], + [13.4575, 0.0741, 13.2718], + [22.8675, 0.0296, 13.87], + [37.0442, 0.0128, 14.2551], + [55.4792, -0.0013, 14.7754] + ] + } + }, + "defaultPushOutVolume": 7 + }, + "t200": { + "uiMaxFlowRate": 194, + "defaultAspirateFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultDispenseFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 58.35, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [1.9331, 3.4604, 3.5588], + [2.9808, 1.5307, 7.2892], + [3.9869, 0.825, 9.3926], + [4.9762, 0.5141, 10.6323], + [5.9431, 0.3232, 11.5819], + [6.9223, 0.2644, 11.9317], + [7.8877, 0.1832, 12.4935], + [8.8562, 0.1512, 12.7463], + [47.7169, 0.0281, 13.836], + [95.63, 0.0007, 15.147], + [211.1169, 0.0005, 15.1655] + ] + } + }, + "dispense": { + "default": { + "1": [ + [1.9331, 3.4604, 3.5588], + [2.9808, 1.5307, 7.2892], + [3.9869, 0.825, 9.3926], + [4.9762, 0.5141, 10.6323], + [5.9431, 0.3232, 11.5819], + [6.9223, 0.2644, 11.9317], + [7.8877, 0.1832, 12.4935], + [8.8562, 0.1512, 12.7463], + [47.7169, 0.0281, 13.836], + [95.63, 0.0007, 15.147], + [211.1169, 0.0005, 15.1655] + ] + } + }, + "defaultPushOutVolume": 5 + }, + "t1000": { + "uiMaxFlowRate": 187.2, + "defaultAspirateFlowRate": { + "default": 160, + "valuesByApiLevel": { "2.14": 160 } + }, + "defaultDispenseFlowRate": { + "default": 160, + "valuesByApiLevel": { "2.14": 160 } + }, + "defaultBlowOutFlowRate": { + "default": 80, + "valuesByApiLevel": { "2.14": 80 } + }, + "defaultFlowAcceleration": 16000.0, + "defaultTipLength": 95.6, + "defaultReturnTipHeight": 0.2, + "aspirate": { + "default": { + "1": [ + [3.9, 1.789, 5.4283], + [5.6991, 0.3019, 11.2278], + [8.5155, 0.2111, 11.7453], + [13.1482, 0.0858, 12.8124], + [17.8909, 0.0604, 13.1472], + [46.0982, 0.0155, 13.9505], + [93.5618, 0.0046, 14.4523], + [112.5991, 0.0023, 14.6687], + [189.5555, 0.002, 14.7035], + [305.5891, 0.001, 14.887], + [431.2836, 0.0004, 15.055], + [625.0209, 0.0003, 15.1309], + [818.6909, 0.0001, 15.2112], + [963.9909, 0.0001, 15.2445], + [992.0791, -0.0005, 15.7723], + [1012.2118, 0.0007, 14.6701], + [1037.1873, 0.0005, 14.8072] + ] + } + }, + "dispense": { + "default": { + "1": [ + [3.9, 1.789, 5.4283], + [5.6991, 0.3019, 11.2278], + [8.5155, 0.2111, 11.7453], + [13.1482, 0.0858, 12.8124], + [17.8909, 0.0604, 13.1472], + [46.0982, 0.0155, 13.9505], + [93.5618, 0.0046, 14.4523], + [112.5991, 0.0023, 14.6687], + [189.5555, 0.002, 14.7035], + [305.5891, 0.001, 14.887], + [431.2836, 0.0004, 15.055], + [625.0209, 0.0003, 15.1309], + [818.6909, 0.0001, 15.2112], + [963.9909, 0.0001, 15.2445], + [992.0791, -0.0005, 15.7723], + [1012.2118, 0.0007, 14.6701], + [1037.1873, 0.0005, 14.8072] + ] + } + }, + "defaultPushOutVolume": 20 + } + }, + "defaultTipOverlapDictionary": { + "default": 10.5, + "opentrons/opentrons_flex_96_tiprack_50ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_1000ul/1": 10.5, + "opentrons/opentrons_flex_96_tiprack_200ul/1": 10.5 + }, + "maxVolume": 1000, + "minVolume": 5, + "defaultTipracks": [ + "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "opentrons/opentrons_flex_96_tiprack_200ul/1", + "opentrons/opentrons_flex_96_tiprack_50ul/1" + ] +} diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json index 9a281ac618cf..465631770011 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t1000": { + "uiMaxFlowRate": 1018.6, "defaultAspirateFlowRate": { "default": 274.7, "valuesByApiLevel": { "2.0": 137.35, "2.6": 274.7 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json index bc51e5751d98..bfb9c6e83e8e 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/2_2.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t1000": { + "uiMaxFlowRate": 1020.7, "defaultAspirateFlowRate": { "default": 274.7, "valuesByApiLevel": { "2.0": 137.35, "2.6": 274.7 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json index 476cb96cc697..e4e765c999c3 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 808.3, "defaultAspirateFlowRate": { "default": 6, "valuesByApiLevel": { "2.14": 6 } @@ -116,6 +117,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 905.7, "defaultAspirateFlowRate": { "default": 80, "valuesByApiLevel": { "2.14": 80 } @@ -228,6 +230,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 787.7, "defaultAspirateFlowRate": { "default": 160, "valuesByApiLevel": { "2.14": 160 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json index 28226b82e4de..f48e41f37f25 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 762.1, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -90,6 +91,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 745, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -178,6 +180,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 763.3, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json index 65456da3a9dc..9c939ba9c7cb 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -70,6 +71,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 802.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -128,6 +130,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 753.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json index 29caae1b15b2..72187981c26e 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p1000/default/3_6.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 785.2, "defaultAspirateFlowRate": { "default": 478, "valuesByApiLevel": { "2.14": 478 } @@ -70,6 +71,7 @@ "defaultPushOutVolume": 7 }, "t200": { + "uiMaxFlowRate": 802.5, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } @@ -128,6 +130,7 @@ "defaultPushOutVolume": 5 }, "t1000": { + "uiMaxFlowRate": 727.3, "defaultAspirateFlowRate": { "default": 716, "valuesByApiLevel": { "2.14": 716 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json index 8acb156a2af3..e10b9ba735d4 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25.3, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } @@ -146,6 +147,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 25.3, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json index 907d45465202..eba2ed05cb1e 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p20/default/2_2.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t20": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } @@ -146,6 +147,7 @@ "defaultPushOutVolume": 0 }, "t10": { + "uiMaxFlowRate": 25, "defaultAspirateFlowRate": { "default": 3.78, "valuesByApiLevel": { "2.0": 3.78, "2.6": 7.56 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json index 14b514edf8d5..0478ea9c0e5c 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p300/default/2_1.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t200": { + "uiMaxFlowRate": 329.3, "defaultAspirateFlowRate": { "default": 46.43, "valuesByApiLevel": { "2.0": 46.43, "2.6": 92.86 } @@ -88,6 +89,7 @@ "defaultPushOutVolume": 0 }, "t300": { + "uiMaxFlowRate": 329.3, "defaultAspirateFlowRate": { "default": 46.43, "valuesByApiLevel": { "2.0": 46.43, "2.6": 92.86 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json index f5492d8809ac..a5d87c485ba6 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json index df9fc3d784b4..464eb2137984 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_4.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 46.3, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json index c798ce421a62..2fca659b0705 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 47, "defaultAspirateFlowRate": { "default": 35, "valuesByApiLevel": { "2.14": 35 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json index 2a292477578b..deae3998fe90 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_3.json @@ -2,6 +2,7 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 31.8, "defaultAspirateFlowRate": { "default": 8, "valuesByApiLevel": { "2.14": 8 } diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json index 771ff88cf22e..397dc63b2304 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_4.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 31.8, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 31.8, + "valuesByApiLevel": { "2.14": 31.8 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json index 644d93354e87..e1b92133bd6d 100644 --- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json @@ -2,17 +2,18 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json", "supportedTips": { "t50": { + "uiMaxFlowRate": 26.7, "defaultAspirateFlowRate": { - "default": 35, - "valuesByApiLevel": { "2.14": 35 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultDispenseFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultBlowOutFlowRate": { - "default": 57, - "valuesByApiLevel": { "2.14": 57 } + "default": 26.7, + "valuesByApiLevel": { "2.14": 26.7 } }, "defaultFlowAcceleration": 1200.0, "defaultTipLength": 57.9, diff --git a/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json b/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json index f7a76e0cde05..a4ba8e659f11 100644 --- a/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json +++ b/shared-data/pipette/schemas/2/pipetteLiquidPropertiesSchema.json @@ -68,13 +68,16 @@ ], "properties": { "defaultAspirateFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultDispenseFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultBlowOutFlowRate": { - "$ref": "#/definitions/flowRate" + "$ref": "#/definitions/flowRate", + "$comment": "for lowVolumeDefault only, the flowRate matches the uiMaxFlowRate. This does not change physical behavior and was made for UI purposes" }, "defaultFlowAcceleration": { "$ref": "#/definitions/positiveNumber" @@ -92,6 +95,11 @@ "dispense": { "type": "array", "items": { "$ref": "#/definitions/liquidHandlingSpecs" } + }, + "uiMaxFlowRate": { + "$ref": "#/definitions/positiveNumber", + "$comment": "To be used in frontend applications only since it is the limit-to-lowest-max", + "description": "An optional number to represent each pipette's supported tip max flow rate for aspirate and dispense. The limit is the lowest max flow rate given all the tip's volumes minus 2% for safety." } } } diff --git a/shared-data/protocol/fixtures/8/simpleFlexV8.json b/shared-data/protocol/fixtures/8/simpleFlexV8.json index 277d7e636fe7..a188ab7c7104 100644 --- a/shared-data/protocol/fixtures/8/simpleFlexV8.json +++ b/shared-data/protocol/fixtures/8/simpleFlexV8.json @@ -1220,8 +1220,8 @@ { "commandType": "loadModule", "params": { - "moduleId": "magneticModuleId", - "model": "magneticModuleV2", + "moduleId": "magneticBlockId", + "model": "magneticBlockV1", "location": { "slotName": "3" } } }, @@ -1254,7 +1254,7 @@ "namespace": "opentrons", "version": 1, "location": { - "moduleId": "magneticModuleId" + "moduleId": "magneticBlockId" }, "displayName": "Sample Collection Plate" } diff --git a/shared-data/protocol/types/schemaV8/index.ts b/shared-data/protocol/types/schemaV8/index.ts index 0a6972fe2714..d501abbe38ee 100644 --- a/shared-data/protocol/types/schemaV8/index.ts +++ b/shared-data/protocol/types/schemaV8/index.ts @@ -4,6 +4,7 @@ import type { LoadedLabware, LoadedModule, Liquid, + RunTimeParameter, } from '../../../js' import type { CommandAnnotation } from '../../../commandAnnotation/types' import type { LabwareDefinition2, RobotType } from '../../../js/types' @@ -136,6 +137,7 @@ export interface ProtocolAnalysisOutput { modules: LoadedModule[] liquids: Liquid[] errors: AnalysisError[] + runTimeParameters: RunTimeParameter[] robotType?: RobotType } diff --git a/shared-data/python/opentrons_shared_data/deck/__init__.py b/shared-data/python/opentrons_shared_data/deck/__init__.py index e922d905ec26..24d56ad730e4 100644 --- a/shared-data/python/opentrons_shared_data/deck/__init__.py +++ b/shared-data/python/opentrons_shared_data/deck/__init__.py @@ -15,9 +15,11 @@ DeckSchemaVersion3, DeckDefinitionV4, DeckSchemaVersion4, + DeckDefinitionV5, + DeckSchemaVersion5, ) -DEFAULT_DECK_DEFINITION_VERSION: Final = 4 +DEFAULT_DECK_DEFINITION_VERSION: Final = 5 class Offset(NamedTuple): @@ -38,6 +40,11 @@ class Offset(NamedTuple): } +@overload +def load(name: str, version: "DeckSchemaVersion5") -> "DeckDefinitionV5": + ... + + @overload def load(name: str, version: "DeckSchemaVersion4") -> "DeckDefinitionV4": ... diff --git a/shared-data/python/opentrons_shared_data/deck/dev_types.py b/shared-data/python/opentrons_shared_data/deck/dev_types.py index 06f372d73bd1..4563ff10953c 100644 --- a/shared-data/python/opentrons_shared_data/deck/dev_types.py +++ b/shared-data/python/opentrons_shared_data/deck/dev_types.py @@ -10,6 +10,7 @@ from ..module.dev_types import ModuleType +DeckSchemaVersion5 = Literal[5] DeckSchemaVersion4 = Literal[4] DeckSchemaVersion3 = Literal[3] DeckSchemaVersion2 = Literal[2] @@ -111,9 +112,11 @@ class Cutout(TypedDict): class CutoutFixture(TypedDict): id: str + expectOpentronsModuleSerialNumber: bool mayMountTo: List[str] displayName: str providesAddressableAreas: Dict[str, List[str]] + fixtureGroup: Dict[str, List[Dict[str, str]]] height: float @@ -176,4 +179,19 @@ class DeckDefinitionV4(_RequiredDeckDefinitionV4, total=False): gripperOffsets: Dict[str, GripperOffsets] -DeckDefinition = Union[DeckDefinitionV3, DeckDefinitionV4] +class _RequiredDeckDefinitionV5(TypedDict): + otId: str + schemaVersion: Literal[5] + cornerOffsetFromOrigin: List[float] + dimensions: List[float] + metadata: Metadata + robot: Robot + locations: LocationsV4 + cutoutFixtures: List[CutoutFixture] + + +class DeckDefinitionV5(_RequiredDeckDefinitionV5, total=False): + gripperOffsets: Dict[str, GripperOffsets] + + +DeckDefinition = Union[DeckDefinitionV3, DeckDefinitionV4, DeckDefinitionV5] diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 203dba1455df..1b2e68040de7 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -160,7 +160,7 @@ class Parameters(BaseModel): loadName: str = Field( ..., description="Name used to reference a labware definition", - pattern=SAFE_STRING_REGEX, + regex=SAFE_STRING_REGEX, ) isMagneticModuleCompatible: bool = Field( ..., @@ -262,7 +262,7 @@ class LabwareDefinition(BaseModel): "(eg myPlate v1/v2/v3). An incrementing integer", ge=1.0, ) - namespace: str = Field(..., pattern=SAFE_STRING_REGEX) + namespace: str = Field(..., regex=SAFE_STRING_REGEX) metadata: Metadata = Field( ..., description="Properties used for search and display" ) diff --git a/shared-data/python/opentrons_shared_data/performance/dev_types.py b/shared-data/python/opentrons_shared_data/performance/dev_types.py new file mode 100644 index 000000000000..842399f2c3ba --- /dev/null +++ b/shared-data/python/opentrons_shared_data/performance/dev_types.py @@ -0,0 +1,56 @@ +"""Type definitions for performance tracking.""" + +from typing import Protocol, TypeVar, Callable, Any +from pathlib import Path +from enum import Enum + +F = TypeVar("F", bound=Callable[..., Any]) + + +class SupportsTracking(Protocol): + """Protocol for classes that support tracking of robot context.""" + + def __init__(self, storage_location: Path, should_track: bool) -> None: + """Initialize the tracker.""" + ... + + def track(self, state: "RobotContextState") -> Callable[[F], F]: + """Decorator to track the given state for the decorated function.""" + ... + + def store(self) -> None: + """Store the tracked data.""" + ... + + +class RobotContextState(Enum): + """Enum representing different states of a robot's operation context.""" + + STARTING_UP = 0, "STARTING_UP" + CALIBRATING = 1, "CALIBRATING" + ANALYZING_PROTOCOL = 2, "ANALYZING_PROTOCOL" + RUNNING_PROTOCOL = 3, "RUNNING_PROTOCOL" + SHUTTING_DOWN = 4, "SHUTTING_DOWN" + + def __init__(self, state_id: int, state_name: str) -> None: + """Initialize the enum member.""" + self.state_id = state_id + self.state_name = state_name + + @classmethod + def from_id(cls, state_id: int) -> "RobotContextState": + """Returns the enum member matching the given state ID. + + Args: + state_id: The ID of the state to retrieve. + + Returns: + RobotContextStates: The enum member corresponding to the given ID. + + Raises: + ValueError: If no matching state is found. + """ + for state in RobotContextState: + if state.state_id == state_id: + return state + raise ValueError(f"Invalid state id: {state_id}") diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index d7f3435ec738..a7b436638846 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -72,17 +72,17 @@ class SupportedTipsDefinition(BaseModel): default_aspirate_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in aspirations by default.", + description="The flowrate used in aspirations by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultAspirateFlowRate", ) default_dispense_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in dispenses by default.", + description="The flowrate used in dispenses by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultDispenseFlowRate", ) default_blowout_flowrate: FlowRateDefinition = Field( ..., - description="The flowrate used in blowouts by default.", + description="The flowrate used in blowouts by default. For lowVolumeDefault only, the flowrate matches uiMaxFlowRate for ui purposes, it does not change physical behavior.", alias="defaultBlowOutFlowRate", ) default_flow_acceleration: float = Field( @@ -111,6 +111,13 @@ class SupportedTipsDefinition(BaseModel): description="The default volume for a push-out during dispense.", alias="defaultPushOutVolume", ) + ui_max_flow_rate: float = Field( + float( + "inf" + ), # some pipettes (GEN1, unreleased prototype models) don't have a max flow rate + description="The lowest volume max flow rate for a pipette's given supported tip, minus 2 percent for safety.", + alias="uiMaxFlowRate", + ) class MotorConfigurations(BaseModel): diff --git a/api/src/opentrons/hardware_control/instruments/instrument_helpers.py b/shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py similarity index 100% rename from api/src/opentrons/hardware_control/instruments/instrument_helpers.py rename to shared-data/python/opentrons_shared_data/pipette/ul_per_mm.py diff --git a/shared-data/python/setup.py b/shared-data/python/setup.py index 8aebebcb408f..4e1720cb610a 100644 --- a/shared-data/python/setup.py +++ b/shared-data/python/setup.py @@ -130,8 +130,6 @@ def get_version(): "Intended Audience :: Science/Research", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering", ] @@ -151,7 +149,7 @@ def get_version(): if __name__ == "__main__": setup( - python_requires=">=3.8", + python_requires=">=3.10", name=DISTNAME, description=DESCRIPTION, license=LICENSE, diff --git a/shared-data/python/tests/deck/test_typechecks.py b/shared-data/python/tests/deck/test_typechecks.py index f021004b0506..4e2406df0fad 100644 --- a/shared-data/python/tests/deck/test_typechecks.py +++ b/shared-data/python/tests/deck/test_typechecks.py @@ -5,7 +5,10 @@ list_names as list_deck_definition_names, load as load_deck_definition, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV3, DeckDefinitionV4 +from opentrons_shared_data.deck.dev_types import ( + DeckDefinitionV3, + DeckDefinitionV5, +) @pytest.mark.parametrize("defname", list_deck_definition_names(version=3)) @@ -14,7 +17,7 @@ def test_v3_defs(defname): typeguard.check_type(defn, DeckDefinitionV3) -@pytest.mark.parametrize("defname", list_deck_definition_names(version=4)) -def test_v4_defs(defname): - defn = load_deck_definition(name=defname, version=4) - typeguard.check_type(defn, DeckDefinitionV4) +@pytest.mark.parametrize("defname", list_deck_definition_names(version=5)) +def test_v5_defs(defname): + defn = load_deck_definition(name=defname, version=5) + typeguard.check_type(defn, DeckDefinitionV5) diff --git a/shared-data/python/tests/labware/test_validations.py b/shared-data/python/tests/labware/test_validations.py new file mode 100644 index 000000000000..39052e5d1508 --- /dev/null +++ b/shared-data/python/tests/labware/test_validations.py @@ -0,0 +1,21 @@ +import pytest + +from pydantic import ValidationError +from opentrons_shared_data.labware import load_definition +from opentrons_shared_data.labware.labware_definition import LabwareDefinition + +from . import get_ot_defs + + +def test_loadname_regex_applied() -> None: + defdict = load_definition(*get_ot_defs()[0]) + defdict["parameters"]["loadName"] = "ALSJHDAKJLA" + with pytest.raises(ValidationError): + LabwareDefinition.parse_obj(defdict) + + +def test_namespace_regex_applied() -> None: + defdict = load_definition(*get_ot_defs()[0]) + defdict["namespace"] = "ALSJHDAKJLA" + with pytest.raises(ValidationError): + LabwareDefinition.parse_obj(defdict) diff --git a/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py new file mode 100644 index 000000000000..ff731ec0e3c8 --- /dev/null +++ b/shared-data/python/tests/pipette/test_max_flow_rates_per_volume.py @@ -0,0 +1,88 @@ +import os +import pytest +from typing import Iterator +from opentrons_shared_data import get_shared_data_root +from opentrons_shared_data.pipette.pipette_load_name_conversions import ( + convert_pipette_model, +) +from opentrons_shared_data.pipette.load_data import load_definition +from opentrons_shared_data.pipette.ul_per_mm import piecewise_volume_conversion + +from opentrons_shared_data.pipette.dev_types import PipetteModel +from opentrons_shared_data.pipette.pipette_definition import ( + ulPerMMDefinition, +) + + +DEFAULT_MAX_SPEED_HIGH_THROUGHPUT_OT3_AXIS_KIND_P = 15 +DEFAULT_MAX_SPEED_LOW_THROUGHPUT_OT3_AXIS_KIND_P = 70 +B_MAX_SPEED = 40 + + +def _get_plunger_max_speed(pipette_model: PipetteModel) -> float: + if "v2" in pipette_model: + return B_MAX_SPEED + else: + if "96" in pipette_model: + return DEFAULT_MAX_SPEED_HIGH_THROUGHPUT_OT3_AXIS_KIND_P + else: + return DEFAULT_MAX_SPEED_LOW_THROUGHPUT_OT3_AXIS_KIND_P + + +def _get_max_flow_rate_at_volume( + ul_per_mm_definition: ulPerMMDefinition, + pipette_model: PipetteModel, + volume: float, +) -> float: + max_speed = _get_plunger_max_speed(pipette_model) + map = list(ul_per_mm_definition.default.values())[-1] + ul_per_mm = piecewise_volume_conversion(volume, map) + return round(ul_per_mm * max_speed, 1) + + +def get_all_pipette_models() -> Iterator[PipetteModel]: + paths_to_validate = ( + get_shared_data_root() / "pipette" / "definitions" / "2" / "liquid" + ) + + _channel_model_str = { + "single_channel": "single", + "ninety_six_channel": "96", + "eight_channel": "multi", + } + assert os.listdir(paths_to_validate), "You have a path wrong" + for channel_dir in os.listdir(paths_to_validate): + for model_dir in os.listdir(paths_to_validate / channel_dir): + for liquid_file in os.listdir(paths_to_validate / channel_dir / model_dir): + for version_file in os.listdir( + paths_to_validate / channel_dir / model_dir / liquid_file + ): + version_list = version_file.split(".json")[0].split("_") + built_model: PipetteModel = PipetteModel( + f"{model_dir}_{_channel_model_str[channel_dir]}_v{version_list[0]}.{version_list[1]}" + ) + if version_list[0] != "1" and version_list[1] != "0": + yield built_model + + +@pytest.mark.parametrize("pipette", list(get_all_pipette_models())) +@pytest.mark.parametrize("action", ["aspirate", "dispense"]) +def test_max_flow_rates_per_volume(pipette: PipetteModel, action: str) -> None: + """Verify the max flow rate values for each pipette's supported tip is in range""" + pipette_model_version = convert_pipette_model(pipette) + definition = load_definition( + pipette_model_version.pipette_type, + pipette_model_version.pipette_channels, + pipette_model_version.pipette_version, + ) + for liquid_name, liquid_properties in definition.liquid_properties.items(): + for ( + tip_type, + supported_tip, + ) in liquid_properties.supported_tips.items(): + assert supported_tip.ui_max_flow_rate < _get_max_flow_rate_at_volume( + supported_tip.aspirate, pipette, liquid_properties.min_volume + ) + assert supported_tip.ui_max_flow_rate < _get_max_flow_rate_at_volume( + supported_tip.dispense, pipette, liquid_properties.min_volume + ) diff --git a/shared-data/webpack.config.js b/shared-data/webpack.config.js deleted file mode 100644 index 18aa64783191..000000000000 --- a/shared-data/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { baseConfig } = require('@opentrons/webpack-config') - -const ENTRY_INDEX = path.join(__dirname, 'js/index.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') - -module.exports = async () => - webpackMerge(baseConfig, { - entry: { index: ENTRY_INDEX }, - output: { - path: OUTPUT_PATH, - filename: 'opentrons-shared-data.js', - library: '@opentrons/shared-data', - libraryTarget: 'umd', - globalObject: 'this', - }, - }) diff --git a/step-generation/src/__tests__/aspirate.test.ts b/step-generation/src/__tests__/aspirate.test.ts index 7731f5e389e2..d937fcda7a47 100644 --- a/step-generation/src/__tests__/aspirate.test.ts +++ b/step-generation/src/__tests__/aspirate.test.ts @@ -67,6 +67,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, } const result = aspirate(params, invariantContext, robotStateWithTip) expect(getSuccessResult(result).commands).toEqual([ @@ -82,6 +84,8 @@ describe('aspirate', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -106,6 +110,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tiprack1Id', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -133,6 +139,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -153,6 +161,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -170,6 +180,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -190,6 +202,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -214,6 +228,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, initialRobotState @@ -246,6 +262,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -278,6 +296,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -316,6 +336,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -348,6 +370,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -386,6 +410,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -414,6 +440,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -441,6 +469,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -468,6 +498,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip @@ -497,6 +529,8 @@ describe('aspirate', () => { well: 'A1', } as AspDispAirgapParams), tipRack: 'tipRack', + xOffset: 0, + yOffset: 0, }, invariantContext, robotStateWithTip diff --git a/step-generation/src/__tests__/blowout.test.ts b/step-generation/src/__tests__/blowout.test.ts index c52cac83042e..8e16cafb3318 100644 --- a/step-generation/src/__tests__/blowout.test.ts +++ b/step-generation/src/__tests__/blowout.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { BlowoutParams } from '@opentrons/shared-data' import type { RobotState, InvariantContext } from '../types' describe('blowout', () => { @@ -24,11 +24,15 @@ describe('blowout', () => { initialRobotState = getInitialRobotStateStandard(invariantContext) robotStateWithTip = getRobotStateWithTipStandard(invariantContext) params = { - pipette: DEFAULT_PIPETTE, - labware: SOURCE_LABWARE, - well: 'A1', + pipetteId: DEFAULT_PIPETTE, + labwareId: SOURCE_LABWARE, + wellName: 'A1', flowRate: 21.1, - offsetFromBottomMm: 1.3, + wellLocation: { + offset: { + z: -1.3, + }, + }, } }) it('blowout with tip', () => { @@ -44,9 +48,9 @@ describe('blowout', () => { wellName: 'A1', flowRate: 21.1, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 1.3, + z: -1.3, }, }, }, @@ -55,7 +59,7 @@ describe('blowout', () => { }) it('blowout with invalid pipette ID should throw error', () => { const result = blowout( - { ...params, pipette: 'badPipette' }, + { ...params, pipetteId: 'badPipette' }, invariantContext, robotStateWithTip ) @@ -63,7 +67,7 @@ describe('blowout', () => { }) it('blowout with invalid labware ID should throw error', () => { const result = blowout( - { ...params, labware: 'badLabware' }, + { ...params, labwareId: 'badLabware' }, invariantContext, robotStateWithTip ) @@ -88,11 +92,15 @@ describe('blowout', () => { const result = blowout( { flowRate: 10, - offsetFromBottomMm: 5, - pipette: DEFAULT_PIPETTE, + wellLocation: { + offset: { + z: -3, + }, + }, + pipetteId: DEFAULT_PIPETTE, volume: 50, - labware: SOURCE_LABWARE, - well: 'A1', + labwareId: SOURCE_LABWARE, + wellName: 'A1', } as BlowoutParams, invariantContext, initialRobotState diff --git a/step-generation/src/__tests__/blowoutUtil.test.ts b/step-generation/src/__tests__/blowoutUtil.test.ts index 33ff37705675..ac2a1c1cd871 100644 --- a/step-generation/src/__tests__/blowoutUtil.test.ts +++ b/step-generation/src/__tests__/blowoutUtil.test.ts @@ -63,11 +63,15 @@ describe('blowoutUtil', () => { blowoutLocation: SOURCE_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.sourceLabwareId, - well: blowoutArgs.sourceWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.sourceLabwareId, + wellName: blowoutArgs.sourceWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries waste chute commands when there is no well', () => { @@ -104,11 +108,15 @@ describe('blowoutUtil', () => { blowoutLocation: DEST_WELL_BLOWOUT_DESTINATION, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: blowoutArgs.destLabwareId, - well: blowoutArgs.destWell, + pipetteId: blowoutArgs.pipette, + labwareId: blowoutArgs.destLabwareId, + wellName: blowoutArgs.destWell, flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil curries blowout with an arbitrary labware Id', () => { @@ -117,11 +125,15 @@ describe('blowoutUtil', () => { blowoutLocation: TROUGH_LABWARE, }) expect(curryCommandCreator).toHaveBeenCalledWith(blowout, { - pipette: blowoutArgs.pipette, - labware: TROUGH_LABWARE, - well: 'A1', + pipetteId: blowoutArgs.pipette, + labwareId: TROUGH_LABWARE, + wellName: 'A1', flowRate: blowoutArgs.flowRate, - offsetFromBottomMm: expect.any(Number), + wellLocation: { + offset: { + z: expect.any(Number), + }, + }, }) }) it('blowoutUtil returns an empty array if not given a blowoutLocation', () => { diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index e43e31c4463a..11b20e652679 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -33,6 +33,8 @@ const airGapHelper = makeAirGapHelper({ origin: 'bottom', offset: { z: 11.54, + x: 0, + y: 0, }, }, }) @@ -98,6 +100,10 @@ beforeEach(() => { blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, tipRack: 'tiprack1Id', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } }) @@ -259,6 +265,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -274,6 +282,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -307,6 +317,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -330,6 +342,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -363,6 +377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -373,6 +389,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -383,6 +401,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -399,6 +419,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -409,6 +431,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -419,6 +443,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -454,6 +480,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -467,6 +495,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -501,6 +531,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -520,6 +552,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +587,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -566,6 +602,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -599,6 +637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -616,6 +656,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -655,6 +697,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -675,6 +719,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +761,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -734,6 +782,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1056,6 +1106,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1080,6 +1132,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1105,6 +1159,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1163,6 +1219,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1188,6 +1246,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1246,6 +1306,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1271,6 +1333,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1313,6 +1377,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1337,6 +1403,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1385,6 +1453,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1409,6 +1479,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1434,6 +1506,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1492,6 +1566,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1517,6 +1593,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1559,6 +1637,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1583,6 +1663,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1627,6 +1709,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1697,6 +1781,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1721,6 +1807,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1746,6 +1834,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1804,6 +1894,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1830,6 +1922,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.1, @@ -1887,6 +1981,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1912,6 +2008,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1954,6 +2052,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1978,6 +2078,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2001,9 +2103,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2041,6 +2143,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2065,6 +2169,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2090,6 +2196,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2148,6 +2256,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2173,6 +2283,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2215,6 +2327,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2239,6 +2353,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2262,9 +2378,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2298,6 +2414,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2365,6 +2483,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2389,6 +2509,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2414,6 +2536,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2472,6 +2596,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2497,6 +2623,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2555,6 +2683,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2580,6 +2710,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2622,6 +2754,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2646,6 +2780,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2669,9 +2805,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2705,6 +2841,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2744,6 +2882,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2769,6 +2909,8 @@ describe('consolidate single-channel', () => { origin: 'bottom', offset: { z: 3.1, + x: 0, + y: 0, }, }, flowRate: 2.2, @@ -2793,6 +2935,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -2851,6 +2995,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -2876,6 +3022,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2918,6 +3066,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2942,6 +3092,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -2965,9 +3117,9 @@ describe('consolidate single-channel', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3000,6 +3152,8 @@ describe('consolidate single-channel', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3058,6 +3212,10 @@ describe('consolidate multi-channel', () => { volume: 140, tipRack: 'tiprack1Id', changeTip: 'once', + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } as ConsolidateArgs const result = consolidate(data, invariantContext, initialRobotState) const res = getSuccessResult(result) diff --git a/step-generation/src/__tests__/dispense.test.ts b/step-generation/src/__tests__/dispense.test.ts index 18e51c9b7a79..1ef07707d80a 100644 --- a/step-generation/src/__tests__/dispense.test.ts +++ b/step-generation/src/__tests__/dispense.test.ts @@ -20,12 +20,11 @@ import { DEFAULT_PIPETTE, SOURCE_LABWARE, } from '../fixtures' -import { dispense } from '../commandCreators/atomic/dispense' -import { InvariantContext, RobotState } from '../types' -import type { - AspDispAirgapParams as V3AspDispAirgapParams, - DispenseParams, -} from '@opentrons/shared-data/protocol/types/schemaV3' +import { + ExtendedDispenseParams, + dispense, +} from '../commandCreators/atomic/dispense' +import type { InvariantContext, RobotState } from '../types' vi.mock('../utils/thermocyclerPipetteCollision') vi.mock('../utils/heaterShakerCollision') @@ -46,7 +45,7 @@ describe('dispense', () => { vi.resetAllMocks() }) describe('tip tracking & commands:', () => { - let params: V3AspDispAirgapParams + let params: ExtendedDispenseParams beforeEach(() => { params = { pipette: DEFAULT_PIPETTE, @@ -55,6 +54,8 @@ describe('dispense', () => { well: 'A1', offsetFromBottomMm: 5, flowRate: 6, + xOffset: 0, + yOffset: 0, } }) it('dispense normally (with tip)', () => { @@ -71,6 +72,8 @@ describe('dispense', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 5, }, }, @@ -99,7 +102,9 @@ describe('dispense', () => { volume: 50, labware: SOURCE_LABWARE, well: 'A1', - } as DispenseParams, + xOffset: 0, + yOffset: 0, + }, invariantContext, initialRobotState ) diff --git a/step-generation/src/__tests__/distribute.test.ts b/step-generation/src/__tests__/distribute.test.ts index 2db91df01d28..6793b9df81e4 100644 --- a/step-generation/src/__tests__/distribute.test.ts +++ b/step-generation/src/__tests__/distribute.test.ts @@ -36,6 +36,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -44,6 +46,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -84,11 +88,15 @@ beforeEach(() => { aspirateAirGapVolume: null, touchTipAfterDispense: false, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } blowoutSingleToTrash = blowoutInPlaceHelper() blowoutSingleToSourceA1 = blowoutHelper(SOURCE_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -96,7 +104,7 @@ beforeEach(() => { }) blowoutSingleToDestA4 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -105,7 +113,7 @@ beforeEach(() => { }) blowoutSingleToDestA3 = blowoutHelper(DEST_LABWARE, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -274,6 +282,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -309,6 +319,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -320,6 +332,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -553,6 +567,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -565,6 +581,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -690,6 +708,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -701,6 +721,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -781,6 +803,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -793,6 +817,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -879,6 +905,8 @@ describe('advanced settings: volume, mix, pre-wet tip, tip touch, tip position', wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/__tests__/mix.test.ts b/step-generation/src/__tests__/mix.test.ts index c2392a94c98a..9fd099a5388d 100644 --- a/step-generation/src/__tests__/mix.test.ts +++ b/step-generation/src/__tests__/mix.test.ts @@ -51,6 +51,10 @@ beforeEach(() => { aspirateDelaySeconds: null, dispenseDelaySeconds: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() @@ -191,7 +195,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -225,7 +229,7 @@ describe('mix: advanced options', () => { dispenseHelper(well, volume), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, @@ -315,7 +319,7 @@ describe('mix: advanced options', () => { delayCommand(12), blowoutHelper(blowoutLabwareId, { wellLocation: { - origin: 'bottom', + origin: 'top', offset: { z: BLOWOUT_OFFSET_ANY, }, diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 49319bfe2eab..b3da39db41d7 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -37,6 +37,8 @@ const airGapHelper = makeAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -45,6 +47,8 @@ const dispenseAirGapHelper = makeDispenseAirGapHelper({ wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -78,6 +82,10 @@ beforeEach(() => { mixInDestination: null, blowoutLocation: null, dropTipLocation: FIXED_TRASH_ID, + aspirateXOffset: 0, + dispenseXOffset: 0, + aspirateYOffset: 0, + dispenseYOffset: 0, } invariantContext = makeContext() @@ -561,6 +569,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -594,6 +604,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -628,6 +640,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -704,6 +718,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -715,6 +731,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -754,6 +772,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -766,6 +786,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -928,6 +950,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -939,6 +963,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -977,6 +1003,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -986,6 +1014,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -997,6 +1027,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -1097,6 +1129,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1122,6 +1156,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1146,6 +1182,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1171,6 +1209,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1195,6 +1235,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1254,6 +1296,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1281,6 +1325,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1303,8 +1349,11 @@ describe('advanced options', () => { wellName: 'B1', wellLocation: { origin: 'bottom', + offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1346,6 +1395,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.2, }, }, @@ -1371,6 +1422,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1408,6 +1461,7 @@ describe('advanced options', () => { key: expect.any(String), params: { pipetteId: 'p300SingleId', + labwareId: 'destPlateId', wellName: 'B1', wellLocation: { @@ -1432,6 +1486,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1457,6 +1513,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1481,6 +1539,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 3.1, }, }, @@ -1540,6 +1600,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -1567,6 +1629,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1591,6 +1655,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1632,6 +1698,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: 3.2, }, }, @@ -1657,6 +1725,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.2, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1716,6 +1786,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1756,6 +1828,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1780,6 +1854,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1805,6 +1881,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1829,6 +1907,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1854,6 +1934,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1913,6 +1995,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -1939,6 +2023,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -1963,6 +2049,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2005,6 +2093,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2029,6 +2119,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2051,9 +2143,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2091,6 +2183,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2115,6 +2209,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2140,6 +2236,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2197,6 +2295,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2222,6 +2322,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, pipetteId: 'p300SingleId', @@ -2248,6 +2350,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2290,6 +2394,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2314,6 +2420,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2336,9 +2444,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2374,6 +2482,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -2442,6 +2552,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2466,6 +2578,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2491,6 +2605,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2515,6 +2631,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2540,6 +2658,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2599,6 +2719,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2625,6 +2747,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2649,6 +2773,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2691,6 +2817,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2715,6 +2843,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2737,9 +2867,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -2777,6 +2907,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2801,6 +2933,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2826,6 +2960,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2885,6 +3021,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -2911,6 +3049,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2935,6 +3075,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -2977,6 +3119,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3001,6 +3145,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3023,9 +3169,9 @@ describe('advanced options', () => { wellName: 'B1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3061,6 +3207,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, @@ -3127,6 +3275,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3151,6 +3301,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3176,6 +3328,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3200,6 +3354,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3225,6 +3381,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3284,6 +3442,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3310,6 +3470,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3334,6 +3496,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3376,6 +3540,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3399,6 +3565,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -3422,9 +3590,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3459,6 +3627,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, volume: 3, @@ -3511,6 +3681,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3535,6 +3707,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3560,6 +3734,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 3.1, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3619,6 +3795,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3644,6 +3822,8 @@ describe('advanced options', () => { wellLocation: { origin: 'bottom', offset: { + x: 0, + y: 0, z: 11.54, }, }, @@ -3669,6 +3849,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3711,6 +3893,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.1, @@ -3735,6 +3919,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: DISPENSE_OFFSET_FROM_BOTTOM_MM, + y: 0, + x: 0, }, }, flowRate: 2.2, @@ -3757,9 +3943,9 @@ describe('advanced options', () => { wellName: 'A1', flowRate: 2.3, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: 13.84, + z: 3.3, }, }, }, @@ -3795,6 +3981,8 @@ describe('advanced options', () => { origin: 'bottom', offset: { z: 11.54, + y: 0, + x: 0, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index fb360c4cebf5..d7226da3387a 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -18,6 +18,8 @@ import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schem import type { CommandCreator, CommandCreatorError } from '../../types' export interface ExtendedAspirateParams extends AspirateParams { + xOffset: number + yOffset: number tipRack: string } /** Aspirate with given args. Requires tip. */ @@ -35,6 +37,8 @@ export const aspirate: CommandCreator = ( flowRate, isAirGap, tipRack, + xOffset, + yOffset, } = args const actionName = 'aspirate' const errors: CommandCreatorError[] = [] @@ -208,6 +212,8 @@ export const aspirate: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index 497257a98d6a..ff3be46d786b 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -1,8 +1,7 @@ import { uuid, getLabwareSlot } from '../../utils' import { COLUMN_4_SLOTS } from '../../constants' import * as errorCreators from '../../errorCreators' -import type { CreateCommand } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' +import type { CreateCommand, BlowoutParams } from '@opentrons/shared-data' import type { CommandCreatorError, CommandCreator } from '../../types' export const blowout: CommandCreator = ( @@ -11,12 +10,13 @@ export const blowout: CommandCreator = ( prevRobotState ) => { /** Blowout with given args. Requires tip. */ - const { pipette, labware, well, offsetFromBottomMm, flowRate } = args + const { pipetteId, labwareId, wellName, wellLocation, flowRate } = args + const actionName = 'blowout' const errors: CommandCreatorError[] = [] - const pipetteData = prevRobotState.pipettes[pipette] + const pipetteData = prevRobotState.pipettes[pipetteId] const slotName = getLabwareSlot( - labware, + labwareId, prevRobotState.labware, prevRobotState.modules ) @@ -27,30 +27,30 @@ export const blowout: CommandCreator = ( errors.push( errorCreators.pipetteDoesNotExist({ actionName, - pipette, + pipette: pipetteId, }) ) } - if (!prevRobotState.tipState.pipettes[pipette]) { + if (!prevRobotState.tipState.pipettes[pipetteId]) { errors.push( errorCreators.noTipOnPipette({ actionName, - pipette, - labware, - well, + pipette: pipetteId, + labware: labwareId, + well: wellName, }) ) } - if (!labware || !prevRobotState.labware[labware]) { + if (!labwareId || !prevRobotState.labware[labwareId]) { errors.push( errorCreators.labwareDoesNotExist({ actionName, - labware, + labware: labwareId, }) ) - } else if (prevRobotState.labware[labware]?.slot === 'offDeck') { + } else if (prevRobotState.labware[labwareId]?.slot === 'offDeck') { errors.push(errorCreators.labwareOffDeck()) } @@ -69,14 +69,14 @@ export const blowout: CommandCreator = ( commandType: 'blowout', key: uuid(), params: { - pipetteId: pipette, - labwareId: labware, - wellName: well, + pipetteId, + labwareId, + wellName, flowRate, wellLocation: { - origin: 'bottom', + origin: 'top', offset: { - z: offsetFromBottomMm, + z: wellLocation?.offset?.z, }, }, }, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index 58c7019fe757..2bec571bd6e6 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -16,8 +16,12 @@ import type { CreateCommand } from '@opentrons/shared-data' import type { DispenseParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' +export interface ExtendedDispenseParams extends DispenseParams { + xOffset: number + yOffset: number +} /** Dispense with given args. Requires tip. */ -export const dispense: CommandCreator = ( +export const dispense: CommandCreator = ( args, invariantContext, prevRobotState @@ -30,6 +34,8 @@ export const dispense: CommandCreator = ( offsetFromBottomMm, flowRate, isAirGap, + xOffset, + yOffset, } = args const actionName = 'dispense' const errors: CommandCreatorError[] = [] @@ -172,6 +178,8 @@ export const dispense: CommandCreator = ( origin: 'bottom', offset: { z: offsetFromBottomMm, + x: xOffset, + y: yOffset, }, }, flowRate, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 6507f9227f2e..b37f2ede1b03 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -152,6 +152,10 @@ export const consolidate: CommandCreator = ( mixFirstAspirate, mixInDestination, dropTipLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const maxWellsPerChunk = Math.floor( @@ -220,6 +224,8 @@ export const consolidate: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -277,6 +283,8 @@ export const consolidate: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, @@ -326,6 +334,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const preWetTipCommands = args.preWetTip // Pre-wet tip is equivalent to a single mix, with volume equal to the consolidate volume. @@ -342,6 +354,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] // can not mix in a waste chute @@ -360,6 +376,10 @@ export const consolidate: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -385,6 +405,8 @@ export const consolidate: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index 9662a07d959c..520ce06aeb46 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -147,6 +147,10 @@ export const distribute: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, blowoutLocation, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -211,6 +215,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, + xOffset: 0, + yOffset: 0, tipRack: args.tipRack, }), ...(aspirateDelay != null @@ -232,6 +238,8 @@ export const distribute: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -290,6 +298,8 @@ export const distribute: CommandCreator = ( well: destWell, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...delayAfterDispenseCommands, ...touchTipAfterDispenseCommand, @@ -337,6 +347,8 @@ export const distribute: CommandCreator = ( offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, tipRack: args.tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -439,6 +451,10 @@ export const distribute: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack: args.tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -478,6 +494,8 @@ export const distribute: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack: args.tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommand, diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index 4a918da5a0df..284529c7c1f2 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -35,6 +35,10 @@ export function mixUtil(args: { aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number tipRack: string + aspirateXOffset: number + dispenseXOffset: number + aspirateYOffset: number + dispenseYOffset: number aspirateDelaySeconds?: number | null | undefined dispenseDelaySeconds?: number | null | undefined }): CurriedCommandCreator[] { @@ -51,6 +55,10 @@ export function mixUtil(args: { aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const getDelayCommand = (seconds?: number | null): CurriedCommandCreator[] => @@ -76,6 +84,8 @@ export function mixUtil(args: { offsetFromBottomMm: aspirateOffsetFromBottomMm, flowRate: aspirateFlowRateUlSec, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ...getDelayCommand(aspirateDelaySeconds), curryCommandCreator(dispense, { @@ -85,6 +95,8 @@ export function mixUtil(args: { well, offsetFromBottomMm: dispenseOffsetFromBottomMm, flowRate: dispenseFlowRateUlSec, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ...getDelayCommand(dispenseDelaySeconds), ], @@ -123,6 +135,10 @@ export const mix: CommandCreator = ( blowoutOffsetFromTopMm, dropTipLocation, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = data const is96Channel = @@ -257,6 +273,10 @@ export const mix: CommandCreator = ( aspirateDelaySeconds, dispenseDelaySeconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) return [ ...configureNozzleLayoutCommand, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index d7f4ec5e181d..2d16c8064bf3 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -205,6 +205,10 @@ export const transfer: CommandCreator = ( dispenseFlowRateUlSec, dispenseOffsetFromBottomMm, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, } = args const aspirateAirGapVolume = args.aspirateAirGapVolume || 0 const dispenseAirGapVolume = args.dispenseAirGapVolume || 0 @@ -329,6 +333,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const mixBeforeAspirateCommands = @@ -346,6 +354,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] const delayAfterAspirateCommands = @@ -410,6 +422,10 @@ export const transfer: CommandCreator = ( aspirateDelaySeconds: aspirateDelay?.seconds, dispenseDelaySeconds: dispenseDelay?.seconds, tipRack, + aspirateXOffset, + aspirateYOffset, + dispenseXOffset, + dispenseYOffset, }) : [] @@ -425,6 +441,8 @@ export const transfer: CommandCreator = ( offsetFromBottomMm: airGapOffsetSourceWell, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ...(aspirateDelay != null ? [ @@ -445,6 +463,8 @@ export const transfer: CommandCreator = ( flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: airGapOffsetDestWell, isAirGap: true, + xOffset: 0, + yOffset: 0, }), ...(dispenseDelay != null ? [ @@ -486,6 +506,8 @@ export const transfer: CommandCreator = ( flowRate: aspirateFlowRateUlSec, offsetFromBottomMm: aspirateOffsetFromBottomMm, tipRack, + xOffset: aspirateXOffset, + yOffset: aspirateYOffset, }), ] const dispenseCommand = [ @@ -496,6 +518,8 @@ export const transfer: CommandCreator = ( well: destinationWell ?? undefined, flowRate: dispenseFlowRateUlSec, offsetFromBottomMm: dispenseOffsetFromBottomMm, + xOffset: dispenseXOffset, + yOffset: dispenseYOffset, }), ] diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index 2c38a361ee7a..3d1ee3945745 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -129,6 +129,8 @@ export const makeAspirateHelper: MakeAspDispHelper = bakedP wellLocation: { origin: 'bottom', offset: { + y: 0, + x: 0, z: ASPIRATE_OFFSET_FROM_BOTTOM_MM, }, }, @@ -199,6 +201,8 @@ const _defaultDispenseParams = { wellLocation: { origin: 'bottom' as const, offset: { + y: 0, + x: 0, z: DISPENSE_OFFSET_FROM_BOTTOM_MM, }, }, diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 98e1e8ec90c8..6cef80c43ed1 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -192,6 +192,10 @@ export type SharedTransferLikeArgs = CommonArgs & { aspirateFlowRateUlSec: number /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number + /** x offset mm */ + aspirateXOffset: number + /** y offset mm */ + aspirateYOffset: number // ===== DISPENSE SETTINGS ===== /** Air gap after dispense */ @@ -206,6 +210,10 @@ export type SharedTransferLikeArgs = CommonArgs & { dispenseFlowRateUlSec: number /** offset from bottom of well in mm */ dispenseOffsetFromBottomMm: number + /** x offset mm */ + dispenseXOffset: number + /** y offset mm */ + dispenseYOffset: number } export type ConsolidateArgs = SharedTransferLikeArgs & { @@ -286,6 +294,12 @@ export type MixArgs = CommonArgs & { /** offset from bottom of well in mm */ aspirateOffsetFromBottomMm: number dispenseOffsetFromBottomMm: number + /** x offset */ + aspirateXOffset: number + dispenseXOffset: number + /** y offset */ + aspirateYOffset: number + dispenseYOffset: number /** flow rates in uL/sec */ aspirateFlowRateUlSec: number dispenseFlowRateUlSec: number diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index c9f36587213c..77d91213d632 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -5,7 +5,6 @@ import reduce from 'lodash/reduce' import { getIsTiprack, getLabwareDefURI, - getWellsDepth, getWellNamePerMultiTip, WASTE_CHUTE_CUTOUT, PipetteChannels, @@ -26,8 +25,8 @@ import { movableTrashCommandsUtil } from './movableTrashCommandsUtil' import type { AddressableAreaName, LabwareDefinition2, + BlowoutParams, } from '@opentrons/shared-data' -import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV4' import type { AdditionalEquipmentEntities, AdditionalEquipmentEntity, @@ -244,15 +243,15 @@ export function getWellsForTips( // the SOURCE_WELL_BLOWOUT_DESTINATION / DEST_WELL_BLOWOUT_DESTINATION // special strings, or to a labware ID. export const blowoutUtil = (args: { - pipette: BlowoutParams['pipette'] + pipette: BlowoutParams['pipetteId'] sourceLabwareId: string - sourceWell: BlowoutParams['well'] + sourceWell: BlowoutParams['wellName'] destLabwareId: string blowoutLocation: string | null | undefined flowRate: number offsetFromTopMm: number invariantContext: InvariantContext - destWell: BlowoutParams['well'] | null + destWell: BlowoutParams['wellName'] | null prevRobotState: RobotState }): CurriedCommandCreator[] => { const { @@ -293,18 +292,18 @@ export const blowoutUtil = (args: { well = trashOrLabware === 'labware' ? 'A1' : null } - const wellDepth = - labware != null && well != null ? getWellsDepth(labware.def, [well]) : 0 - - const offsetFromBottomMm = wellDepth + offsetFromTopMm if (well != null && trashOrLabware === 'labware' && labware != null) { return [ curryCommandCreator(blowout, { - pipette: pipette, - labware: labware.id, - well, + pipetteId: pipette, + labwareId: labware.id, + wellName: well, flowRate, - offsetFromBottomMm, + wellLocation: { + offset: { + z: offsetFromTopMm, + }, + }, }), ] } else if (trashOrLabware === 'wasteChute') { @@ -479,6 +478,8 @@ interface DispenseLocationHelperArgs { pipetteId: string volume: number flowRate: number + xOffset: number + yOffset: number offsetFromBottomMm?: number well?: string } @@ -494,6 +495,8 @@ export const dispenseLocationHelper: CommandCreator flowRate, offsetFromBottomMm, well, + xOffset, + yOffset, } = args const trashOrLabware = getTrashOrLabware( @@ -516,6 +519,8 @@ export const dispenseLocationHelper: CommandCreator well, flowRate, offsetFromBottomMm, + xOffset, + yOffset, }), ] } else if (trashOrLabware === 'wasteChute') { @@ -660,6 +665,8 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + xOffset: 0, + yOffset: 0, }), ] // when aspirating out of multi wells for consolidate @@ -674,6 +681,9 @@ export const airGapHelper: CommandCreator = ( offsetFromBottomMm, isAirGap: true, tipRack, + // NOTE: airgap aspirates happen at default x/y offset + xOffset: 0, + yOffset: 0, }), ] } diff --git a/system-server/Pipfile b/system-server/Pipfile index 78c13a0ff55f..d1ce7f43f6ac 100644 --- a/system-server/Pipfile +++ b/system-server/Pipfile @@ -14,6 +14,7 @@ pydantic = "==1.10.12" importlib-metadata = ">=4.13.0,<5" sqlalchemy = "==1.4.51" pyjwt = "==2.6.0" +filetype = "==1.2.0" systemd-python = { version = "==234", markers="sys_platform == 'linux'" } server-utils = {editable = true, path = "./../server-utils"} system_server = {path = ".", editable = true} diff --git a/system-server/Pipfile.lock b/system-server/Pipfile.lock index bbaa48e640c3..d7d315362f2c 100644 --- a/system-server/Pipfile.lock +++ b/system-server/Pipfile.lock @@ -50,6 +50,14 @@ "markers": "python_version >= '3.7'", "version": "==0.99.1" }, + "filetype": { + "hashes": [ + "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", + "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25" + ], + "index": "pypi", + "version": "==1.2.0" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -231,12 +239,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.11.0" }, "uvicorn": { "hashes": [ diff --git a/system-server/settings_schema.json b/system-server/settings_schema.json index c16b2c496215..7916f39dcf9e 100644 --- a/system-server/settings_schema.json +++ b/system-server/settings_schema.json @@ -9,6 +9,19 @@ "default": "/var/lib/opentrons-system-server/", "env_names": ["ot_system_server_persistence_directory"], "type": "string" + }, + "oem_mode_enabled": { + "title": "OEM Mode Enabled", + "description": "A flag used to change the default splash screen on system startup. If this flag is disabled (default), the Opentrons loading video will be shown. If this flag is enabled but `oem_mode_splash_custom` is not set, then the default OEM Mode splash screen will be shown. If this flag is enabled and `oem_mode_splash_custom` is set to a PNG filepath, the custom splash screen will be shown.", + "default": false, + "env_names": ["ot_system_server_oem_mode_enabled"], + "type": "bool" + }, + "oem_mode_splash_custom": { + "description": "The filepath of the PNG image used as the custom splash screen. Read the description of the `oem_mode_enabled` flag to know how the splash screen changes when the flag is enabled/disabled.", + "default": null, + "env_names": ["ot_system_server_oem_mode_splash_custom"], + "type": "string" } }, "additionalProperties": false diff --git a/system-server/system_server/settings/__init__.py b/system-server/system_server/settings/__init__.py index b2db58a63899..feae773340f7 100644 --- a/system-server/system_server/settings/__init__.py +++ b/system-server/system_server/settings/__init__.py @@ -1,6 +1,10 @@ """system_server.settings: Provides an interface to get server settings.""" -from .settings import get_settings, SystemServerSettings +from .settings import ( + save_settings, + get_settings, + SystemServerSettings, +) -__all__ = ["get_settings", "SystemServerSettings"] +__all__ = ["save_settings", "get_settings", "SystemServerSettings"] diff --git a/system-server/system_server/settings/settings.py b/system-server/system_server/settings/settings.py index a042b76b91d8..32e34079ebda 100644 --- a/system-server/system_server/settings/settings.py +++ b/system-server/system_server/settings/settings.py @@ -1,27 +1,20 @@ """System server configuration options.""" import typing -import logging from functools import lru_cache from pydantic import BaseSettings, Field -from dotenv import load_dotenv - -log = logging.getLogger(__name__) +from dotenv import load_dotenv, set_key @lru_cache(maxsize=1) def get_settings() -> "SystemServerSettings": """Get the settings.""" - update_from_dotenv() - return SystemServerSettings() - - -def update_from_dotenv() -> None: - """Get the location of the settings file.""" env = Environment().dot_env_path if env: load_dotenv(env) + return SystemServerSettings() + class Environment(BaseSettings): """Environment related settings.""" @@ -56,7 +49,44 @@ class SystemServerSettings(BaseSettings): ), ) + oem_mode_enabled: typing.Optional[bool] = Field( + default=False, + description=( + "A flag used to change the default splash screen on system startup." + " If this flag is disabled (default), the Opentrons loading video will be shown." + " If this flag is enabled but `oem_mode_splash_custom` is not set," + " then the default OEM Mode splash screen will be shown." + " If this flag is enabled and `oem_mode_splash_custom` is set to a" + " PNG filepath, the custom splash screen will be shown." + ), + ) + + oem_mode_splash_custom: typing.Optional[str] = Field( + default=None, + description=( + "The filepath of the PNG image used as the custom splash screen." + " Read the description of the `oem_mode_enabled` flag to know how" + " the splash screen changes when the flag is enabled/disabled." + ), + ) + class Config: """Prefix configuration for environment variables.""" + env_file = Environment().dot_env_path env_prefix = "OT_SYSTEM_SERVER_" + + +def save_settings(settings: SystemServerSettings) -> bool: + """Save the settings to the dotenv file.""" + env_path = Environment().dot_env_path + env_path = env_path or f"{settings.persistence_directory}/system.env" + prefix = settings.Config.env_prefix + try: + for key, val in settings.dict().items(): + name = f"{prefix}{key}" + value = str(val) if val is not None else "" + set_key(env_path, name, value) + return True + except (IOError, ValueError): + return False diff --git a/system-server/system_server/system/oem_mode/__init__.py b/system-server/system_server/system/oem_mode/__init__.py new file mode 100644 index 000000000000..ddbd3555ebfd --- /dev/null +++ b/system-server/system_server/system/oem_mode/__init__.py @@ -0,0 +1,5 @@ +"""OEM Mode endpoint.""" + +from .router import oem_mode_router + +__all__ = ["oem_mode_router"] diff --git a/system-server/system_server/system/oem_mode/dependencies.py b/system-server/system_server/system/oem_mode/dependencies.py new file mode 100644 index 000000000000..b59bb825024e --- /dev/null +++ b/system-server/system_server/system/oem_mode/dependencies.py @@ -0,0 +1,21 @@ +"""Dependencies for /system/register endpoints.""" +from system_server.jwt import Registrant +from fastapi import Query + + +def create_registrant( + subject: str = Query( + ..., description="Identifies the human intending to register with the robot" + ), + agent: str = Query(..., description="Identifies the app type making the request"), + agentId: str = Query( + ..., description="A unique identifier for the instance of the agent" + ), +) -> Registrant: + """Define a unique Registrant to create a registration token for. + + A registrant is defined by a set of unique identifiers that remain + persistent indefinitely for the same person using the same method of + access to the system. + """ + return Registrant(subject=subject, agent=agent, agent_id=agentId) diff --git a/system-server/system_server/system/oem_mode/models.py b/system-server/system_server/system/oem_mode/models.py new file mode 100644 index 000000000000..192e1ce4fa6c --- /dev/null +++ b/system-server/system_server/system/oem_mode/models.py @@ -0,0 +1,9 @@ +"""Models for /system/oem_mode.""" + +from pydantic import BaseModel, Field + + +class EnableOEMMode(BaseModel): + """Enable OEM Mode model.""" + + enable: bool = Field(..., description="Enable or Disable OEM Mode.") diff --git a/system-server/system_server/system/oem_mode/router.py b/system-server/system_server/system/oem_mode/router.py new file mode 100644 index 000000000000..0f3b9aa52f4f --- /dev/null +++ b/system-server/system_server/system/oem_mode/router.py @@ -0,0 +1,132 @@ +"""Router for /system/register endpoint.""" + +import os +import filetype # type: ignore[import-untyped] +from fastapi import ( + APIRouter, + Depends, + status, + Response, + UploadFile, + File, + HTTPException, +) + +from .models import EnableOEMMode +from ...settings import SystemServerSettings, get_settings, save_settings + + +oem_mode_router = APIRouter() + + +@oem_mode_router.put( + "/system/oem_mode/enable", + summary="Enable or Disable OEM Mode for this robot.", + responses={ + status.HTTP_200_OK: {"message": "OEM Mode changed successfully."}, + status.HTTP_400_BAD_REQUEST: {"message": "OEM Mode did not changed."}, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + "message": "OEM Mode unhandled exception." + }, + }, +) +async def enable_oem_mode_endpoint( + response: Response, + enableRequest: EnableOEMMode, + settings: SystemServerSettings = Depends(get_settings), +) -> Response: + """Router for /system/oem_mode/enable endpoint.""" + enable = enableRequest.enable + try: + settings.oem_mode_enabled = enable + success = save_settings(settings) + response.status_code = ( + status.HTTP_200_OK if success else status.HTTP_400_BAD_REQUEST + ) + except Exception: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + return response + + +@oem_mode_router.post( + "/system/oem_mode/upload_splash", + summary="Upload an image to be used as the boot up splash screen.", + responses={ + status.HTTP_201_CREATED: {"message": "OEM Mode splash screen uploaded"}, + status.HTTP_400_BAD_REQUEST: {"message": "OEM Mode splash screen not set"}, + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: { + "message": "File is larger than 5mb" + }, + status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: {"message": "Invalid file type"}, + status.HTTP_500_INTERNAL_SERVER_ERROR: { + "message": "OEM Mode splash unhandled exception." + }, + }, +) +async def upload_splash_image( + response: Response, + file: UploadFile = File(...), + settings: SystemServerSettings = Depends(get_settings), +) -> Response: + """Router for /system/oem_mode/upload_splash endpoint.""" + # Make sure oem mode is enabled before this request + if not settings.oem_mode_enabled: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="OEM Mode needs to be enabled to upload splash image.", + ) + + # Get the file info + file_info = filetype.guess(file.file) + if file_info is None: + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unable to determine file type", + ) + + # Only accept PNG files + accepted_file_types = ["image/png", "png"] + content_type = file_info.extension.lower() + if ( + file.content_type not in accepted_file_types + or content_type not in accepted_file_types + ): + raise HTTPException( + status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + detail="Unsupported file type", + ) + + file_size = 0 + for chunk in file.file: + file_size += len(chunk) + if file_size > 5 * 1024 * 1024: # 5MB + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail="File is larger than 5mb.", + ) + + # TODO: Validate image dimensions + + # return the pointer back to the starting point so that the next read starts from the starting point + await file.seek(0) + + try: + # Remove the old image if exists + if settings.oem_mode_splash_custom: + os.unlink(settings.oem_mode_splash_custom) + + # file is valid, save to final location + filepath = f"{settings.persistence_directory}/{file.filename}" + with open(filepath, "wb+") as f: + f.write(file.file.read()) + + # store the file location to settings and save the dotenv + settings.oem_mode_splash_custom = filepath + success = save_settings(settings) + response.status_code = ( + status.HTTP_201_CREATED if success else status.HTTP_400_BAD_REQUEST + ) + except Exception: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + + return response diff --git a/system-server/system_server/system/router.py b/system-server/system_server/system/router.py index 0ae868c5f51e..b138a1d0ed62 100644 --- a/system-server/system_server/system/router.py +++ b/system-server/system_server/system/router.py @@ -3,6 +3,7 @@ from .register.router import register_router from .authorize.router import authorize_router from .connected.router import connected_router +from .oem_mode.router import oem_mode_router system_router = APIRouter() @@ -11,3 +12,5 @@ system_router.include_router(router=authorize_router) system_router.include_router(router=connected_router) + +system_router.include_router(router=oem_mode_router) diff --git a/system-server/tests/integration/resources/oem_mode_custom.png b/system-server/tests/integration/resources/oem_mode_custom.png new file mode 100644 index 000000000000..14cf4ac12bd3 Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_custom.png differ diff --git a/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png b/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png new file mode 100644 index 000000000000..2cc51a01cb07 Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_wrong_dimensions.png differ diff --git a/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg b/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg new file mode 100644 index 000000000000..aa95d031d93d Binary files /dev/null and b/system-server/tests/integration/resources/oem_mode_wrong_image_type.jpeg differ diff --git a/system-server/tests/integration/test_oem_mode.tavern.yaml b/system-server/tests/integration/test_oem_mode.tavern.yaml new file mode 100644 index 000000000000..9778495c6c3f --- /dev/null +++ b/system-server/tests/integration/test_oem_mode.tavern.yaml @@ -0,0 +1,124 @@ +--- +test_name: Test enable/disable OEM Mode +marks: + - usefixtures: + - run_server +stages: + - name: PUT first request + request: &enable_oem_mode_first + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + enable: true + method: PUT + headers: + content-type: application/json + response: + status_code: 200 + - name: PUT second request + request: &enable_oem_mode_second + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + enable: false + method: PUT + headers: + content-type: application/json + response: + status_code: 200 + - name: PUT third request + request: &enable_oem_mode_third + url: "{host:s}:{port:d}/system/oem_mode/enable" + json: + wrong_key: false + method: PUT + headers: + content-type: application/json + response: + status_code: 422 +--- +test_name: Upload, and validate a good image for OEM Mode + +marks: + - usefixtures: + - run_server +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: &upload_splash_first + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 + +--- +test_name: Dont process upload_splash request if oem mode is disabled + +marks: + - usefixtures: + - run_server + +stages: + - name: Disable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": false + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 403 + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 +--- +test_name: Validate the image before processing + +marks: + - usefixtures: + - run_server + +stages: + - name: Enable OEM Mode + request: + url: "{host:s}:{port:d}/system/oem_mode/enable" + method: PUT + json: + "enable": true + - name: Upload non-PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_wrong_image_type.jpeg' + response: + status_code: 415 + - name: Upload a PNG Image + request: + url: "{host:s}:{port:d}/system/oem_mode/upload_splash" + method: POST + files: + file: 'tests/integration/resources/oem_mode_custom.png' + response: + status_code: 201 diff --git a/test-data-generation/.flake8 b/test-data-generation/.flake8 new file mode 100644 index 000000000000..4aa1c02d7aa7 --- /dev/null +++ b/test-data-generation/.flake8 @@ -0,0 +1,25 @@ +[flake8] + +# max cyclomatic complexity +max-complexity = 9 + +extend-ignore = + # defer formatting concerns to black + # E203: space around `:` operator + # E501: maximum line length + E203, + E501, + # do not require type annotations for self nor cls + ANN101, + ANN102 + # do not require docstring for __init__, put them on the class + D107, + +# configure flake8-docstrings +# https://pypi.org/project/flake8-docstrings/ +docstring-convention = google + +noqa-require-code = true + +per-file-ignores = + setup.py:ANN,D \ No newline at end of file diff --git a/test-data-generation/Makefile b/test-data-generation/Makefile new file mode 100644 index 000000000000..03c881dbf894 --- /dev/null +++ b/test-data-generation/Makefile @@ -0,0 +1,32 @@ +include ../scripts/python.mk + +.PHONY: lint +lint: + $(python) -m black --check . + $(python) -m flake8 . + $(python) -m mypy . + +.PHONY: format +format: + $(python) -m black . + +.PHONY: setup +setup: + $(pipenv) sync --dev + +.PHONY: teardown +teardown: + $(pipenv) --rm + +.PHONY: clean +clean: + rm -rf build dist *.egg-info .mypy_cache .pytest_cache src/test_data_generation.egg-info + +.PHONY: wheel +wheel: + $(python) setup.py $(wheel_opts) bdist_wheel + rm -rf build + +.PHONY: test +test: + $(pytest) tests -vvv \ No newline at end of file diff --git a/test-data-generation/Pipfile b/test-data-generation/Pipfile new file mode 100644 index 000000000000..758bcddacb7c --- /dev/null +++ b/test-data-generation/Pipfile @@ -0,0 +1,20 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +pytest = "==7.4.3" +black = "==23.11.0" +mypy = "==1.7.1" +flake8 = "==7.0.0" +flake8-annotations = "~=3.0.1" +flake8-docstrings = "~=1.7.0" +flake8-noqa = "~=1.4.0" +hypothesis = "==6.96.1" +opentrons-shared-data = {file = "../shared-data/python", editable = true} +test-data-generation = {file = ".", editable = true} + + +[requires] +python_version = "3.10" diff --git a/test-data-generation/Pipfile.lock b/test-data-generation/Pipfile.lock new file mode 100644 index 000000000000..1b223033d616 --- /dev/null +++ b/test-data-generation/Pipfile.lock @@ -0,0 +1,365 @@ +{ + "_meta": { + "hash": { + "sha256": "1df89f797a19f2c0febc582e7452a52858511cece041f9f612a59d35628226c2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==23.11.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", + "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.1" + }, + "flake8": { + "hashes": [ + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==7.0.0" + }, + "flake8-annotations": { + "hashes": [ + "sha256:af78e3216ad800d7e144745ece6df706c81b3255290cbf870e54879d495e8ade", + "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==3.0.1" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af", + "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.7.0" + }, + "flake8-noqa": { + "hashes": [ + "sha256:4465e16a19be433980f6f563d05540e2e54797eb11facb9feb50fed60624dc45", + "sha256:771765ab27d1efd157528379acd15131147f9ae578a72d17fb432ca197881243" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.4.0" + }, + "hypothesis": { + "hashes": [ + "sha256:848ea0952f0bdfd02eac59e41b03f1cbba8fa2cffeffa8db328bbd6cfe159974", + "sha256:955a57e56be4607c81c17ca53e594af54aadeed91e07b88bb7f84e8208ea7739" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.96.1" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "jsonschema": { + "hashes": [ + "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", + "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + ], + "markers": "python_version >= '3.7'", + "version": "==4.17.3" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340", + "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49", + "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82", + "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce", + "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb", + "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51", + "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5", + "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e", + "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7", + "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33", + "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9", + "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1", + "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6", + "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a", + "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe", + "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7", + "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200", + "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7", + "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a", + "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28", + "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea", + "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120", + "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d", + "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42", + "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea", + "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2", + "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.7.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "opentrons-shared-data": { + "editable": true, + "file": "../shared-data/python", + "markers": "python_version >= '3.8'" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pydantic": { + "hashes": [ + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + ], + "markers": "python_version >= '3.7'", + "version": "==1.10.15" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "pyrsistent": { + "hashes": [ + "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", + "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", + "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", + "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", + "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca", + "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", + "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", + "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", + "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714", + "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf", + "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", + "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", + "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", + "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d", + "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054", + "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", + "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", + "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423", + "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", + "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", + "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", + "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0", + "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", + "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", + "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce", + "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", + "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", + "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86", + "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f", + "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", + "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98", + "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "test-data-generation": { + "editable": true, + "file": "." + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + ], + "markers": "python_version < '3.11'", + "version": "==4.11.0" + } + }, + "develop": {} +} diff --git a/test-data-generation/mypy.ini b/test-data-generation/mypy.ini new file mode 100644 index 000000000000..b94476cbcaa8 --- /dev/null +++ b/test-data-generation/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +show_error_codes = True +warn_unused_configs = True +strict = True +exclude = setup.py \ No newline at end of file diff --git a/test-data-generation/pytest.ini b/test-data-generation/pytest.ini new file mode 100644 index 000000000000..49f044127468 --- /dev/null +++ b/test-data-generation/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --color=yes --strict-markers +asyncio_mode = auto diff --git a/test-data-generation/setup.py b/test-data-generation/setup.py new file mode 100755 index 000000000000..4246340dd68a --- /dev/null +++ b/test-data-generation/setup.py @@ -0,0 +1,91 @@ +# Inspired by: +# https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ +import sys +import codecs +import os +import os.path +from setuptools import setup, find_packages + +# make stdout blocking since Travis sets it to nonblocking +if os.name == "posix": + import fcntl + + flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL) + fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + +HERE = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(HERE, "..", "scripts")) + +from python_build_utils import normalize_version # noqa: E402 + + +def get_version(): + buildno = os.getenv("BUILD_NUMBER") + project = os.getenv("OPENTRONS_PROJECT", "robot-stack") + git_dir = os.getenv("OPENTRONS_GIT_DIR", None) + if buildno: + normalize_opts = {"extra_tag": buildno} + else: + normalize_opts = {} + return normalize_version( + "test-data-generation", project, git_dir=git_dir, **normalize_opts + ) + + +VERSION = get_version() + +DISTNAME = "test_data_generation" +LICENSE = "Apache 2.0" +AUTHOR = "Opentrons" +EMAIL = "engineering@opentrons.com" +URL = "https://github.com/Opentrons/opentrons" +DOWNLOAD_URL = "" +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", +] +KEYWORDS = ["robots", "protocols", "synbio", "pcr", "automation", "lab"] +DESCRIPTION = "Library for working with test data on the Opentrons robots" +PACKAGES = find_packages(where="src", exclude=["tests.*", "tests"]) +INSTALL_REQUIRES = [ + f"opentrons-shared-data=={VERSION}", +] + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +if __name__ == "__main__": + setup( + python_requires="~=3.10", + name=DISTNAME, + description=DESCRIPTION, + license=LICENSE, + url=URL, + version=VERSION, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + keywords=KEYWORDS, + long_description=__doc__, + packages=PACKAGES, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + package_dir={"": "src"}, + package_data={"test-data-generation": ["py.typed"]}, + ) diff --git a/tsconfig-eslint.json b/tsconfig-eslint.json index 4468d4f6fd4d..541feb786c0f 100644 --- a/tsconfig-eslint.json +++ b/tsconfig-eslint.json @@ -19,6 +19,7 @@ "labware-designer/typings", "labware-library/src", "labware-library/typings", + "opentrons-ai-client/src", "shared-data/deck", "shared-data/js", "shared-data/protocol", diff --git a/usb-bridge/node-client/webpack.config.js b/usb-bridge/node-client/webpack.config.js deleted file mode 100644 index c01e57beb07b..000000000000 --- a/usb-bridge/node-client/webpack.config.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -const path = require('path') -const webpackMerge = require('webpack-merge') -const { DefinePlugin } = require('webpack') -const { nodeBaseConfig } = require('@opentrons/webpack-config') -const { versionForProject } = require('../../scripts/git-version') - -const ENTRY_INDEX = path.join(__dirname, 'src/index.ts') -const ENTRY_CLI = path.join(__dirname, 'src/cli.ts') -const OUTPUT_PATH = path.join(__dirname, 'lib') -const project = process.env.OPENTRONS_PROJECT ?? 'robot-stack' - -module.exports = async () => - webpackMerge(nodeBaseConfig, { - entry: { - index: ENTRY_INDEX, - cli: ENTRY_CLI, - }, - output: { path: OUTPUT_PATH }, - plugins: [ - new DefinePlugin({ - _PKG_VERSION_: JSON.stringify(await versionForProject(project)), - }), - ], - }) diff --git a/yarn.lock b/yarn.lock index 82f657d3a480..0c770fd6b174 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,33 +37,33 @@ dependencies: default-browser-id "3.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" - integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== dependencies: - "@babel/highlight" "^7.23.4" - chalk "^2.4.2" + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5": - version "7.23.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" - integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" + integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== "@babel/core@>=7.2.2", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.20.12", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.3", "@babel/core@^7.23.5": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.0.tgz#56cbda6b185ae9d9bed369816a8f4423c5f2ff1b" - integrity sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717" + integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.4" "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-module-transforms" "^7.23.3" - "@babel/helpers" "^7.24.0" - "@babel/parser" "^7.24.0" + "@babel/helpers" "^7.24.4" + "@babel/parser" "^7.24.4" "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.0" + "@babel/traverse" "^7.24.1" "@babel/types" "^7.24.0" convert-source-map "^2.0.0" debug "^4.1.0" @@ -71,14 +71,14 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.0", "@babel/generator@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" - integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== +"@babel/generator@^7.23.0", "@babel/generator@^7.24.1", "@babel/generator@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498" + integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw== dependencies: - "@babel/types" "^7.23.6" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" + "@babel/types" "^7.24.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" "@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.22.5": @@ -95,7 +95,7 @@ dependencies: "@babel/types" "^7.22.15" -"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== @@ -106,17 +106,17 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.23.6": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.0.tgz#fc7554141bdbfa2d17f7b4b80153b9b090e5d158" - integrity sha512-QAH+vfvts51BCsNZ2PhY6HAggnlS6omLLFTsIpeqZk/MmJ6cW7tgz5yRv0fMJThcr6FmbMrENh1RgrWPTYA76g== +"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3" + integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-function-name" "^7.23.0" "@babel/helper-member-expression-to-functions" "^7.23.0" "@babel/helper-optimise-call-expression" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-replace-supers" "^7.24.1" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" semver "^6.3.1" @@ -130,10 +130,10 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz#465805b7361f461e86c680f1de21eaf88c25901b" - integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q== +"@babel/helper-define-polyfill-provider@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" + integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -161,19 +161,19 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-member-expression-to-functions@^7.22.15", "@babel/helper-member-expression-to-functions@^7.23.0": +"@babel/helper-member-expression-to-functions@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== dependencies: "@babel/types" "^7.23.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" - integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5", "@babel/helper-module-imports@^7.24.1": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" + integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== dependencies: - "@babel/types" "^7.22.15" + "@babel/types" "^7.24.0" "@babel/helper-module-transforms@^7.23.3": version "7.23.3" @@ -207,13 +207,13 @@ "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-wrap-function" "^7.22.20" -"@babel/helper-replace-supers@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" - integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== +"@babel/helper-replace-supers@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz#7085bd19d4a0b7ed8f405c1ed73ccb70f323abc1" + integrity sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ== dependencies: "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-member-expression-to-functions" "^7.23.0" "@babel/helper-optimise-call-expression" "^7.22.5" "@babel/helper-simple-access@^7.22.5": @@ -238,16 +238,16 @@ "@babel/types" "^7.22.5" "@babel/helper-string-parser@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" - integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5": +"@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== @@ -261,52 +261,61 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" -"@babel/helpers@^7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.0.tgz#a3dd462b41769c95db8091e49cfe019389a9409b" - integrity sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA== +"@babel/helpers@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" + integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== dependencies: "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.0" + "@babel/traverse" "^7.24.1" "@babel/types" "^7.24.0" -"@babel/highlight@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" - integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== dependencies: "@babel/helper-validator-identifier" "^7.22.20" chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.6", "@babel/parser@^7.24.0", "@babel/parser@^7.8.3": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.0.tgz#26a3d1ff49031c53a97d03b604375f028746a9ac" - integrity sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4", "@babel/parser@^7.8.3": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a" - integrity sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ== +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" + integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d" - integrity sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ== +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" + integrity sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz#da8261f2697f0f41b0855b91d3a20a1fbfd271d3" + integrity sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.23.3" + "@babel/plugin-transform-optional-chaining" "^7.24.1" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7": - version "7.23.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b" - integrity sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw== +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz#1181d9685984c91d657b8ddf14f0487a6bab2988" + integrity sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw== dependencies: "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -348,26 +357,26 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz#084564e0f3cc21ea6c70c44cff984a1c0509729a" - integrity sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA== +"@babel/plugin-syntax-flow@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz#875c25e3428d7896c87589765fc8b9d32f24bd8d" + integrity sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-syntax-import-assertions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz#9c05a7f592982aff1a2768260ad84bcd3f0c77fc" - integrity sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw== +"@babel/plugin-syntax-import-assertions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz#db3aad724153a00eaac115a3fb898de544e34971" + integrity sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-syntax-import-attributes@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06" - integrity sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA== +"@babel/plugin-syntax-import-attributes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz#c66b966c63b714c4eec508fcf5763b1f2d381093" + integrity sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" @@ -383,12 +392,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz#8f2e4f8a9b5f9aa16067e142c1ac9cd9f810f473" - integrity sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg== +"@babel/plugin-syntax-jsx@^7.22.5", "@babel/plugin-syntax-jsx@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz#3f6ca04b8c841811dbc3c5c5f837934e0d626c10" + integrity sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" @@ -446,12 +455,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz#24f460c85dbbc983cd2b9c4994178bcc01df958f" - integrity sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ== +"@babel/plugin-syntax-typescript@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz#b3bcc51f396d15f3591683f90239de143c076844" + integrity sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -461,220 +470,220 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz#94c6dcfd731af90f27a79509f9ab7fb2120fc38b" - integrity sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ== +"@babel/plugin-transform-arrow-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz#2bf263617060c9cc45bcdbf492b8cc805082bf27" + integrity sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-async-generator-functions@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz#9adaeb66fc9634a586c5df139c6240d41ed801ce" - integrity sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ== +"@babel/plugin-transform-async-generator-functions@^7.24.3": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz#8fa7ae481b100768cc9842c8617808c5352b8b89" + integrity sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg== dependencies: "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-transform-async-to-generator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa" - integrity sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw== +"@babel/plugin-transform-async-to-generator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz#0e220703b89f2216800ce7b1c53cb0cf521c37f4" + integrity sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw== dependencies: - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-module-imports" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-remap-async-to-generator" "^7.22.20" -"@babel/plugin-transform-block-scoped-functions@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77" - integrity sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A== +"@babel/plugin-transform-block-scoped-functions@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz#1c94799e20fcd5c4d4589523bbc57b7692979380" + integrity sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-block-scoping@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz#b2d38589531c6c80fbe25e6b58e763622d2d3cf5" - integrity sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw== +"@babel/plugin-transform-block-scoping@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012" + integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-class-properties@^7.22.5", "@babel/plugin-transform-class-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48" - integrity sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg== +"@babel/plugin-transform-class-properties@^7.22.5", "@babel/plugin-transform-class-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz#bcbf1aef6ba6085cfddec9fc8d58871cf011fc29" + integrity sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-class-static-block@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz#2a202c8787a8964dd11dfcedf994d36bfc844ab5" - integrity sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ== +"@babel/plugin-transform-class-static-block@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4" + integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-transform-classes@^7.23.8": - version "7.23.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz#d08ae096c240347badd68cdf1b6d1624a6435d92" - integrity sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg== +"@babel/plugin-transform-classes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz#5bc8fc160ed96378184bc10042af47f50884dcb1" + integrity sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474" - integrity sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw== +"@babel/plugin-transform-computed-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz#bc7e787f8e021eccfb677af5f13c29a9934ed8a7" + integrity sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/template" "^7.22.15" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/template" "^7.24.0" -"@babel/plugin-transform-destructuring@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311" - integrity sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw== +"@babel/plugin-transform-destructuring@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz#b1e8243af4a0206841973786292b8c8dd8447345" + integrity sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-dotall-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50" - integrity sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ== +"@babel/plugin-transform-dotall-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz#d56913d2f12795cc9930801b84c6f8c47513ac13" + integrity sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-duplicate-keys@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce" - integrity sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA== +"@babel/plugin-transform-duplicate-keys@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz#5347a797fe82b8d09749d10e9f5b83665adbca88" + integrity sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-dynamic-import@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz#c7629e7254011ac3630d47d7f34ddd40ca535143" - integrity sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ== +"@babel/plugin-transform-dynamic-import@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz#2a5a49959201970dd09a5fca856cb651e44439dd" + integrity sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-transform-exponentiation-operator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18" - integrity sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ== +"@babel/plugin-transform-exponentiation-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz#6650ebeb5bd5c012d5f5f90a26613a08162e8ba4" + integrity sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-export-namespace-from@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz#084c7b25e9a5c8271e987a08cf85807b80283191" - integrity sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ== +"@babel/plugin-transform-export-namespace-from@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz#f033541fc036e3efb2dcb58eedafd4f6b8078acd" + integrity sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-transform-flow-strip-types@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz#cfa7ca159cc3306fab526fc67091556b51af26ff" - integrity sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q== +"@babel/plugin-transform-flow-strip-types@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz#fa8d0a146506ea195da1671d38eed459242b2dcc" + integrity sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-flow" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-flow" "^7.24.1" -"@babel/plugin-transform-for-of@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz#81c37e24171b37b370ba6aaffa7ac86bcb46f94e" - integrity sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw== +"@babel/plugin-transform-for-of@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz#67448446b67ab6c091360ce3717e7d3a59e202fd" + integrity sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-function-name@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc" - integrity sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw== +"@babel/plugin-transform-function-name@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz#8cba6f7730626cc4dfe4ca2fa516215a0592b361" + integrity sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA== dependencies: - "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-function-name" "^7.23.0" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-json-strings@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz#a871d9b6bd171976efad2e43e694c961ffa3714d" - integrity sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg== +"@babel/plugin-transform-json-strings@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz#08e6369b62ab3e8a7b61089151b161180c8299f7" + integrity sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-transform-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4" - integrity sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ== +"@babel/plugin-transform-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz#0a1982297af83e6b3c94972686067df588c5c096" + integrity sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-logical-assignment-operators@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz#e599f82c51d55fac725f62ce55d3a0886279ecb5" - integrity sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg== +"@babel/plugin-transform-logical-assignment-operators@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz#719d8aded1aa94b8fb34e3a785ae8518e24cfa40" + integrity sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc" - integrity sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag== +"@babel/plugin-transform-member-expression-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz#896d23601c92f437af8b01371ad34beb75df4489" + integrity sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-modules-amd@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d" - integrity sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw== +"@babel/plugin-transform-modules-amd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz#b6d829ed15258536977e9c7cc6437814871ffa39" + integrity sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ== dependencies: "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-modules-commonjs@^7.23.0", "@babel/plugin-transform-modules-commonjs@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" - integrity sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA== +"@babel/plugin-transform-modules-commonjs@^7.23.0", "@babel/plugin-transform-modules-commonjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz#e71ba1d0d69e049a22bf90b3867e263823d3f1b9" + integrity sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw== dependencies: "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-simple-access" "^7.22.5" -"@babel/plugin-transform-modules-systemjs@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz#105d3ed46e4a21d257f83a2f9e2ee4203ceda6be" - integrity sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw== +"@babel/plugin-transform-modules-systemjs@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz#2b9625a3d4e445babac9788daec39094e6b11e3e" + integrity sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA== dependencies: "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/plugin-transform-modules-umd@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9" - integrity sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg== +"@babel/plugin-transform-modules-umd@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz#69220c66653a19cf2c0872b9c762b9a48b8bebef" + integrity sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg== dependencies: "@babel/helper-module-transforms" "^7.23.3" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": version "7.22.5" @@ -684,223 +693,223 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-new-target@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980" - integrity sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ== +"@babel/plugin-transform-new-target@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz#29c59988fa3d0157de1c871a28cd83096363cc34" + integrity sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz#45556aad123fc6e52189ea749e33ce090637346e" - integrity sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA== +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz#0cd494bb97cb07d428bd651632cb9d4140513988" + integrity sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-transform-numeric-separator@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz#03d08e3691e405804ecdd19dd278a40cca531f29" - integrity sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q== +"@babel/plugin-transform-numeric-separator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz#5bc019ce5b3435c1cadf37215e55e433d674d4e8" + integrity sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-transform-object-rest-spread@^7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.0.tgz#7b836ad0088fdded2420ce96d4e1d3ed78b71df1" - integrity sha512-y/yKMm7buHpFFXfxVFS4Vk1ToRJDilIa6fKRioB9Vjichv58TDGXTvqV0dN7plobAmTW5eSEGXDngE+Mm+uO+w== +"@babel/plugin-transform-object-rest-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz#5a3ce73caf0e7871a02e1c31e8b473093af241ff" + integrity sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA== dependencies: - "@babel/compat-data" "^7.23.5" "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.23.3" + "@babel/plugin-transform-parameters" "^7.24.1" -"@babel/plugin-transform-object-super@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd" - integrity sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA== +"@babel/plugin-transform-object-super@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz#e71d6ab13483cca89ed95a474f542bbfc20a0520" + integrity sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-replace-supers" "^7.22.20" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-replace-supers" "^7.24.1" -"@babel/plugin-transform-optional-catch-binding@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz#318066de6dacce7d92fa244ae475aa8d91778017" - integrity sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A== +"@babel/plugin-transform-optional-catch-binding@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz#92a3d0efe847ba722f1a4508669b23134669e2da" + integrity sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.23.0", "@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz#6acf61203bdfc4de9d4e52e64490aeb3e52bd017" - integrity sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA== +"@babel/plugin-transform-optional-chaining@^7.23.0", "@babel/plugin-transform-optional-chaining@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz#26e588acbedce1ab3519ac40cc748e380c5291e6" + integrity sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-transform-parameters@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af" - integrity sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw== +"@babel/plugin-transform-parameters@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz#983c15d114da190506c75b616ceb0f817afcc510" + integrity sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-private-methods@^7.22.5", "@babel/plugin-transform-private-methods@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4" - integrity sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g== +"@babel/plugin-transform-private-methods@^7.22.5", "@babel/plugin-transform-private-methods@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz#a0faa1ae87eff077e1e47a5ec81c3aef383dc15a" + integrity sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw== dependencies: - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-private-property-in-object@^7.23.4": - version "7.23.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz#3ec711d05d6608fd173d9b8de39872d8dbf68bf5" - integrity sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A== +"@babel/plugin-transform-private-property-in-object@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz#756443d400274f8fb7896742962cc1b9f25c1f6a" + integrity sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.24.1" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875" - integrity sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw== +"@babel/plugin-transform-property-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz#d6a9aeab96f03749f4eebeb0b6ea8e90ec958825" + integrity sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-transform-react-jsx-self@^7.18.6", "@babel/plugin-transform-react-jsx-self@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz#ed3e7dadde046cce761a8e3cf003a13d1a7972d9" - integrity sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ== + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.1.tgz#a21d866d8167e752c6a7c4555dba8afcdfce6268" + integrity sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/plugin-transform-react-jsx-source@^7.19.6", "@babel/plugin-transform-react-jsx-source@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz#03527006bdc8775247a78643c51d4e715fe39a3e" - integrity sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g== + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz#a2dedb12b09532846721b5df99e52ef8dc3351d0" + integrity sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-regenerator@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c" - integrity sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ== +"@babel/plugin-transform-regenerator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz#625b7545bae52363bdc1fbbdc7252b5046409c8c" + integrity sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" regenerator-transform "^0.15.2" -"@babel/plugin-transform-reserved-words@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8" - integrity sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg== +"@babel/plugin-transform-reserved-words@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz#8de729f5ecbaaf5cf83b67de13bad38a21be57c1" + integrity sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-shorthand-properties@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210" - integrity sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg== +"@babel/plugin-transform-shorthand-properties@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz#ba9a09144cf55d35ec6b93a32253becad8ee5b55" + integrity sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-spread@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c" - integrity sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg== +"@babel/plugin-transform-spread@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz#a1acf9152cbf690e4da0ba10790b3ac7d2b2b391" + integrity sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" -"@babel/plugin-transform-sticky-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04" - integrity sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg== +"@babel/plugin-transform-sticky-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz#f03e672912c6e203ed8d6e0271d9c2113dc031b9" + integrity sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-template-literals@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07" - integrity sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg== +"@babel/plugin-transform-template-literals@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz#15e2166873a30d8617e3e2ccadb86643d327aab7" + integrity sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-typeof-symbol@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4" - integrity sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ== +"@babel/plugin-transform-typeof-symbol@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz#6831f78647080dec044f7e9f68003d99424f94c7" + integrity sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-typescript@^7.23.3": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz#aa36a94e5da8d94339ae3a4e22d40ed287feb34c" - integrity sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA== +"@babel/plugin-transform-typescript@^7.24.1": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz#03e0492537a4b953e491f53f2bc88245574ebd15" + integrity sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g== dependencies: "@babel/helper-annotate-as-pure" "^7.22.5" - "@babel/helper-create-class-features-plugin" "^7.23.6" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/plugin-syntax-typescript" "^7.23.3" + "@babel/helper-create-class-features-plugin" "^7.24.4" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/plugin-syntax-typescript" "^7.24.1" -"@babel/plugin-transform-unicode-escapes@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925" - integrity sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q== +"@babel/plugin-transform-unicode-escapes@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz#fb3fa16676549ac7c7449db9b342614985c2a3a4" + integrity sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-property-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad" - integrity sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA== +"@babel/plugin-transform-unicode-property-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz#56704fd4d99da81e5e9f0c0c93cabd91dbc4889e" + integrity sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc" - integrity sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw== +"@babel/plugin-transform-unicode-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz#57c3c191d68f998ac46b708380c1ce4d13536385" + integrity sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" -"@babel/plugin-transform-unicode-sets-regex@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e" - integrity sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw== +"@babel/plugin-transform-unicode-sets-regex@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz#c1ea175b02afcffc9cf57a9c4658326625165b7f" + integrity sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.22.15" - "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-plugin-utils" "^7.24.0" "@babel/preset-env@^7.23.2": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.0.tgz#11536a7f4b977294f0bdfad780f01a8ac8e183fc" - integrity sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b" + integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A== dependencies: - "@babel/compat-data" "^7.23.5" + "@babel/compat-data" "^7.24.4" "@babel/helper-compilation-targets" "^7.23.6" "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.23.3" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.23.3" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.23.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-class-properties" "^7.12.13" "@babel/plugin-syntax-class-static-block" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.23.3" - "@babel/plugin-syntax-import-attributes" "^7.23.3" + "@babel/plugin-syntax-import-assertions" "^7.24.1" + "@babel/plugin-syntax-import-attributes" "^7.24.1" "@babel/plugin-syntax-import-meta" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" @@ -912,69 +921,69 @@ "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.23.3" - "@babel/plugin-transform-async-generator-functions" "^7.23.9" - "@babel/plugin-transform-async-to-generator" "^7.23.3" - "@babel/plugin-transform-block-scoped-functions" "^7.23.3" - "@babel/plugin-transform-block-scoping" "^7.23.4" - "@babel/plugin-transform-class-properties" "^7.23.3" - "@babel/plugin-transform-class-static-block" "^7.23.4" - "@babel/plugin-transform-classes" "^7.23.8" - "@babel/plugin-transform-computed-properties" "^7.23.3" - "@babel/plugin-transform-destructuring" "^7.23.3" - "@babel/plugin-transform-dotall-regex" "^7.23.3" - "@babel/plugin-transform-duplicate-keys" "^7.23.3" - "@babel/plugin-transform-dynamic-import" "^7.23.4" - "@babel/plugin-transform-exponentiation-operator" "^7.23.3" - "@babel/plugin-transform-export-namespace-from" "^7.23.4" - "@babel/plugin-transform-for-of" "^7.23.6" - "@babel/plugin-transform-function-name" "^7.23.3" - "@babel/plugin-transform-json-strings" "^7.23.4" - "@babel/plugin-transform-literals" "^7.23.3" - "@babel/plugin-transform-logical-assignment-operators" "^7.23.4" - "@babel/plugin-transform-member-expression-literals" "^7.23.3" - "@babel/plugin-transform-modules-amd" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-modules-systemjs" "^7.23.9" - "@babel/plugin-transform-modules-umd" "^7.23.3" + "@babel/plugin-transform-arrow-functions" "^7.24.1" + "@babel/plugin-transform-async-generator-functions" "^7.24.3" + "@babel/plugin-transform-async-to-generator" "^7.24.1" + "@babel/plugin-transform-block-scoped-functions" "^7.24.1" + "@babel/plugin-transform-block-scoping" "^7.24.4" + "@babel/plugin-transform-class-properties" "^7.24.1" + "@babel/plugin-transform-class-static-block" "^7.24.4" + "@babel/plugin-transform-classes" "^7.24.1" + "@babel/plugin-transform-computed-properties" "^7.24.1" + "@babel/plugin-transform-destructuring" "^7.24.1" + "@babel/plugin-transform-dotall-regex" "^7.24.1" + "@babel/plugin-transform-duplicate-keys" "^7.24.1" + "@babel/plugin-transform-dynamic-import" "^7.24.1" + "@babel/plugin-transform-exponentiation-operator" "^7.24.1" + "@babel/plugin-transform-export-namespace-from" "^7.24.1" + "@babel/plugin-transform-for-of" "^7.24.1" + "@babel/plugin-transform-function-name" "^7.24.1" + "@babel/plugin-transform-json-strings" "^7.24.1" + "@babel/plugin-transform-literals" "^7.24.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.1" + "@babel/plugin-transform-member-expression-literals" "^7.24.1" + "@babel/plugin-transform-modules-amd" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-modules-systemjs" "^7.24.1" + "@babel/plugin-transform-modules-umd" "^7.24.1" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.23.3" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.23.4" - "@babel/plugin-transform-numeric-separator" "^7.23.4" - "@babel/plugin-transform-object-rest-spread" "^7.24.0" - "@babel/plugin-transform-object-super" "^7.23.3" - "@babel/plugin-transform-optional-catch-binding" "^7.23.4" - "@babel/plugin-transform-optional-chaining" "^7.23.4" - "@babel/plugin-transform-parameters" "^7.23.3" - "@babel/plugin-transform-private-methods" "^7.23.3" - "@babel/plugin-transform-private-property-in-object" "^7.23.4" - "@babel/plugin-transform-property-literals" "^7.23.3" - "@babel/plugin-transform-regenerator" "^7.23.3" - "@babel/plugin-transform-reserved-words" "^7.23.3" - "@babel/plugin-transform-shorthand-properties" "^7.23.3" - "@babel/plugin-transform-spread" "^7.23.3" - "@babel/plugin-transform-sticky-regex" "^7.23.3" - "@babel/plugin-transform-template-literals" "^7.23.3" - "@babel/plugin-transform-typeof-symbol" "^7.23.3" - "@babel/plugin-transform-unicode-escapes" "^7.23.3" - "@babel/plugin-transform-unicode-property-regex" "^7.23.3" - "@babel/plugin-transform-unicode-regex" "^7.23.3" - "@babel/plugin-transform-unicode-sets-regex" "^7.23.3" + "@babel/plugin-transform-new-target" "^7.24.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.1" + "@babel/plugin-transform-numeric-separator" "^7.24.1" + "@babel/plugin-transform-object-rest-spread" "^7.24.1" + "@babel/plugin-transform-object-super" "^7.24.1" + "@babel/plugin-transform-optional-catch-binding" "^7.24.1" + "@babel/plugin-transform-optional-chaining" "^7.24.1" + "@babel/plugin-transform-parameters" "^7.24.1" + "@babel/plugin-transform-private-methods" "^7.24.1" + "@babel/plugin-transform-private-property-in-object" "^7.24.1" + "@babel/plugin-transform-property-literals" "^7.24.1" + "@babel/plugin-transform-regenerator" "^7.24.1" + "@babel/plugin-transform-reserved-words" "^7.24.1" + "@babel/plugin-transform-shorthand-properties" "^7.24.1" + "@babel/plugin-transform-spread" "^7.24.1" + "@babel/plugin-transform-sticky-regex" "^7.24.1" + "@babel/plugin-transform-template-literals" "^7.24.1" + "@babel/plugin-transform-typeof-symbol" "^7.24.1" + "@babel/plugin-transform-unicode-escapes" "^7.24.1" + "@babel/plugin-transform-unicode-property-regex" "^7.24.1" + "@babel/plugin-transform-unicode-regex" "^7.24.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.1" "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.8" - babel-plugin-polyfill-corejs3 "^0.9.0" - babel-plugin-polyfill-regenerator "^0.5.5" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" core-js-compat "^3.31.0" semver "^6.3.1" "@babel/preset-flow@^7.22.15": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.0.tgz#0de60271b0a439b415501c5b28f685fbcb080e1c" - integrity sha512-cum/nSi82cDaSJ21I4PgLTVlj0OXovFk6GRguJYe/IKg6y6JHLTbJhybtX4k35WT9wdeJfEVjycTixMhBHd0Dg== + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.1.tgz#da7196c20c2d7dd4e98cfd8b192fe53b5eb6f0bb" + integrity sha512-sWCV2G9pcqZf+JHyv/RyqEIpFypxdCSxWIxQjpdaQxenNog7cN1pr76hg8u0Fz8Qgg0H4ETkGcJnXL8d4j0PPA== dependencies: "@babel/helper-plugin-utils" "^7.24.0" "@babel/helper-validator-option" "^7.23.5" - "@babel/plugin-transform-flow-strip-types" "^7.23.3" + "@babel/plugin-transform-flow-strip-types" "^7.24.1" "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" @@ -986,15 +995,15 @@ esutils "^2.0.2" "@babel/preset-typescript@^7.23.0": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz#14534b34ed5b6d435aa05f1ae1c5e7adcc01d913" - integrity sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ== + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz#89bdf13a3149a17b3b2a2c9c62547f06db8845ec" + integrity sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ== dependencies: - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.15" - "@babel/plugin-syntax-jsx" "^7.23.3" - "@babel/plugin-transform-modules-commonjs" "^7.23.3" - "@babel/plugin-transform-typescript" "^7.23.3" + "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-validator-option" "^7.23.5" + "@babel/plugin-syntax-jsx" "^7.24.1" + "@babel/plugin-transform-modules-commonjs" "^7.24.1" + "@babel/plugin-transform-typescript" "^7.24.1" "@babel/register@^7.22.15": version "7.23.7" @@ -1013,17 +1022,17 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs3@^7.12.1": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.0.tgz#34243e29e369a762dd2a356fee65c3767973828a" - integrity sha512-HxiRMOncx3ly6f3fcZ1GVKf+/EROcI9qwPgmij8Czqy6Okm/0T37T4y2ZIlLUuEUFjtM7NRsfdCO8Y3tAiJZew== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.4.tgz#b9ebe728087cfbb22bbaccc6f9a70d69834124a0" + integrity sha512-VOQOexSilscN24VEY810G/PqtpFvx/z6UqDIjIWbDe2368HhDLkYN5TYwaEz/+eRCUkhJ2WaNLLmQAlxzfWj4w== dependencies: core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" - integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== dependencies: regenerator-runtime "^0.14.0" @@ -1036,23 +1045,23 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" -"@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" - integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== +"@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" + integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/generator" "^7.23.6" + "@babel/code-frame" "^7.24.1" + "@babel/generator" "^7.24.1" "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.24.0" + "@babel/parser" "^7.24.1" "@babel/types" "^7.24.0" debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.4.4": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== @@ -1081,87 +1090,87 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== -"@csstools/cascade-layer-name-parser@^1.0.8": - version "1.0.8" - resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.8.tgz#24d841d80e78f6c2970a36d53e6b58e8fcea41f6" - integrity sha512-xHxXavWvXB5nAA9IvZtjEzkONM3hPXpxqYK4cEw60LcqPiFjq7ZlEFxOyYFPrG4UdANKtnucNtRVDy7frjq6AA== +"@csstools/cascade-layer-name-parser@^1.0.9": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.9.tgz#7093f9c26fd92dee87d853a97de0647c5a8c4262" + integrity sha512-RRqNjxTZDUhx7pxYOBG/AkCVmPS3zYzfE47GEhIGkFuWFTQGJBgWOUUkKNo5MfxIfjDz5/1L3F3rF1oIsYaIpw== -"@csstools/color-helpers@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-4.0.0.tgz#a1d6ffcefe5c1d389cbcca15f46da3cdaf241443" - integrity sha512-wjyXB22/h2OvxAr3jldPB7R7kjTUEzopvjitS8jWtyd8fN6xJ8vy1HnHu0ZNfEkqpBJgQ76Q+sBDshWcMvTa/w== +"@csstools/color-helpers@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-4.2.0.tgz#e8629ca9dce03a3a309506e7892b7f862673cf85" + integrity sha512-hJJrSBzbfGxUsaR6X4Bzd/FLx0F1ulKnR5ljY9AiXCtsR+H+zSWQDFWlKES1BRaVZTDHLpIIHS9K2o0h+JLlrg== "@csstools/convert-colors@^1.4.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== -"@csstools/css-calc@^1.1.7": - version "1.1.7" - resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-1.1.7.tgz#89b5cde81ecb4686d9abd66b7eb54015cf39c442" - integrity sha512-+7bUzB5I4cI97tKmBJA8ilTl/YRo6VAOdlrnd/4x2NyK60nvYurGKa5TZpE1zcgIrTC97iJRE0/V65feyFytuw== +"@csstools/css-calc@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-1.2.0.tgz#a45145a868e644c31c79baf74c8de64fd09b3415" + integrity sha512-iQqIW5vDPqQdLx07/atCuNKDprhIWjB0b8XRhUyXZWBZYUG+9mNyFwyu30rypX84WLevVo25NYW2ipxR8WyseQ== -"@csstools/css-color-parser@^1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-1.5.2.tgz#4fdf8e23960b4724913f7cbfd4f413eb8f35724b" - integrity sha512-5GEkuuUxD5dael3xoWjyf7gAPAi4pwm8X8JW/nUMhxntGY4Wo4Lp7vKlex4V5ZgTfAoov14rZFsZyOantdTatg== +"@csstools/css-color-parser@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-2.0.0.tgz#8e75d1b4a857317f537b3c0a223be0ef1735bbdb" + integrity sha512-0/v6OPpcg+b8TJT2N1Rcp0oH5xEvVOU5K2qDkaR3IMHNXuJ7XfVCQLINt3Cuj8mr54DbilEoZ9uvAmHBoZ//Fw== dependencies: - "@csstools/color-helpers" "^4.0.0" - "@csstools/css-calc" "^1.1.7" + "@csstools/color-helpers" "^4.2.0" + "@csstools/css-calc" "^1.2.0" -"@csstools/css-parser-algorithms@^2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.0.tgz#b45d3c7cbdd4214261724c82f96e33c746fedd58" - integrity sha512-YfEHq0eRH98ffb5/EsrrDspVWAuph6gDggAE74ZtjecsmyyWpW768hOyiONa8zwWGbIWYfa2Xp4tRTrpQQ00CQ== +"@csstools/css-parser-algorithms@^2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.1.tgz#c45440d1efa2954006748a01697072dae5881bcd" + integrity sha512-ubEkAaTfVZa+WwGhs5jbo5Xfqpeaybr/RvWzvFxRs4jfq16wH8l8Ty/QEEpINxll4xhuGfdMbipRyz5QZh9+FA== -"@csstools/css-tokenizer@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz#b099d543ea57b64f495915a095ead583866c50c6" - integrity sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg== +"@csstools/css-tokenizer@^2.2.4": + version "2.2.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.4.tgz#a4b8718ed7fcd2dcd555de16b31ca59ad4b96a06" + integrity sha512-PuWRAewQLbDhGeTvFuq2oClaSCKPIBmHyIobCV39JHRYN0byDcUWJl5baPeNUcqrjtdMNqFooE0FGl31I3JOqw== -"@csstools/media-query-list-parser@^2.1.8": - version "2.1.8" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.8.tgz#36157fbe54ea30d5f2b1767c69fcdf92048a7b1d" - integrity sha512-DiD3vG5ciNzeuTEoh74S+JMjQDs50R3zlxHnBnfd04YYfA/kh2KiBCGhzqLxlJcNq+7yNQ3stuZZYLX6wK/U2g== +"@csstools/media-query-list-parser@^2.1.9": + version "2.1.9" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.9.tgz#feb4b7268f998956eb3ded69507869e73d005dda" + integrity sha512-qqGuFfbn4rUmyOB0u8CVISIp5FfJ5GAR3mBrZ9/TKndHakdnm6pY0L/fbLcpPnrzwCyyTEZl1nUcXAYHEWneTA== "@csstools/postcss-cascade-layers@^4.0.1": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.3.tgz#2805dbb8dec661101928298b2e16599edf3c2bea" - integrity sha512-RbkQoOH23yGhWVetgBTwFgIOHEyU2tKMN7blTz/YAKKabR6tr9pP7mYS23Q9snFY2hr8WSaV8Le64KdM9BtUSA== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-4.0.4.tgz#0f20882d4f528a8128b0855ce63c3e6eee6d1b44" + integrity sha512-MKErv8lpEwVmAcAwidY1Kfd3oWrh2Q14kxHs9xn26XzjP/PrcdngWq63lJsZeMlBY7o+WlEOeE+FP6zPzeY2uw== dependencies: - "@csstools/selector-specificity" "^3.0.2" + "@csstools/selector-specificity" "^3.0.3" postcss-selector-parser "^6.0.13" "@csstools/postcss-color-function@^3.0.7": - version "3.0.10" - resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-3.0.10.tgz#708d34f24daf5ff9978d2d4e8d3413f638a41158" - integrity sha512-jxiXmSl4ZYX8KewFjL5ef6of9uW73VkaHeDb2tqb5q4ZDPYxjusNX1KJ8UXY8+7ydqS5QBo42tVMrSMGy+rDmw== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "3.0.14" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-3.0.14.tgz#b148f611626a0b6dfd66319b7921b7419b2318a0" + integrity sha512-joGAf5bT3Jg1CpybupMJ4DwNg/VNjmLWZoWMDmX0MTy/ftHA1Qr4+CslqTT4AA1n6Dx4Wa+DSMGPrDLHtRP0jg== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" "@csstools/postcss-color-mix-function@^2.0.7": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.10.tgz#fd86d1f3b334fb59a3558d33f121ce5dff758da8" - integrity sha512-zeD856+FDCUjB077pPS+Z9OnTQnqpiJrao3TW+sasCb/gJ3vZCX7sRSRFsRUo0/MntTtJu9hkKv9eMkFmfjydA== + version "2.0.14" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-2.0.14.tgz#5baec0e7b4ef488e830ef702c85f1d45ef2193df" + integrity sha512-ZLbgtdhyuOoWoRo/W8jFv68q+IMgTJHOAI+WunRbrRPqI+vJ0K2rud/lS9Se5urzM/imVKs/kz0Uobm5Yj4HUg== dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" "@csstools/postcss-exponential-functions@^1.0.1": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.4.tgz#c8c3773d4f761428717b80803302722ed2f849f1" - integrity sha512-frMf0CFVnZoGEKAHlxLy3s4g/tpjyFn5+A+h895UJNm9Uc+ewGT7+EeK7Kh9IHH4pD4FkaGW1vOQtER00PLurQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-1.0.5.tgz#ac6f9e545cf6bbf9d6bad11e655ca693c4982e58" + integrity sha512-7S7I7KgwHWQYzJJAoIjRtUf7DQs1dxipeg1A6ikZr0PYapNJX7UHz0evlpE67SQqYj1xBs70gpG7xUv3uLp4PA== dependencies: - "@csstools/css-calc" "^1.1.7" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/css-calc" "^1.2.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/postcss-font-format-keywords@^3.0.0": version "3.0.2" @@ -1172,42 +1181,42 @@ postcss-value-parser "^4.2.0" "@csstools/postcss-gamut-mapping@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.3.tgz#e5323fb1bf46f6d32d760e98028a8e9da9d8fe4b" - integrity sha512-P0+ude1KyCy9LXOe2pHJmpcXK4q/OQbr2Sn2wQSssMw0rALGmny2MfHiCqEu8n6mf2cN6lWDZdzY8enBk8WHXQ== + version "1.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-1.0.7.tgz#6413d3f9e2612e9419cc9978aa36f87895f74e88" + integrity sha512-vrsHsl5TN6NB5CT0rPG6JE9V2GLFftcmPtF/k4cWT4gyVMCsDyS9wEVl82sgvh/JQ32TaUo6bh8Ndl+XRJqGQw== dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/postcss-gradients-interpolation-method@^4.0.7": - version "4.0.11" - resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.11.tgz#4e6cf5d6917672058d532d963c709e3776b9ab36" - integrity sha512-LFom5jCVUfzF+iuiOZvhvX7RRN8vc+tKpcKo9s4keEBAU2mPwV5/Fgz5iylEfXP/DZbEdq2C0At20urMi/lupw== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "4.0.15" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-4.0.15.tgz#6cb6785733da39101e02a8803d374c2fd9c45c2b" + integrity sha512-0xQ5r4WU/6W2lDmnOTx9liC1Cq6RSnrkEzqX7d0cRA3fz5hjC276pA0nLMoAiY3vtAp0u71nTk/3TRdnCx/OUw== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" "@csstools/postcss-hwb-function@^3.0.6": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.9.tgz#15c5b8d43cffe62283b6175494188d6957712d91" - integrity sha512-S3/Z+mGHWIKAex7DLsHFDiku5lBEK34avT2My6sGPNCXB38TZjrKI0rd7JdN9oulem5sn+CU7oONyIftui24oQ== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "3.0.13" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-3.0.13.tgz#b83c0a4aa962c7182eececaec3d88cc152e8c16a" + integrity sha512-f44tgkFSxJBGm8UjlkAfBP7xE2x2XFFdvNdedHl8jpx2pQcW8a50OT3yeMnM3NB9Y2Ynd7Wn8iXARiV/IHoKvw== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" "@csstools/postcss-ic-unit@^3.0.2": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.4.tgz#9f4bffaed6ece2a79e1e15fbd7ba6aea8d61c851" - integrity sha512-OB6ojl33/TQHhjVx1NI+n3EnYbdUM6Q/mSUv3WFATdcz7IrH/CmBaZt7P1R6j1Xdp58thIa6jm4Je7saGs+2AA== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-3.0.6.tgz#441f18a9064884e1e6ab77169413e0e6184f5c1d" + integrity sha512-fHaU9C/sZPauXMrzPitZ/xbACbvxbkPpHoUgB9Kw5evtsBWdVkVrajOyiT9qX7/c+G1yjApoQjP1fQatldsy9w== dependencies: - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" postcss-value-parser "^4.2.0" @@ -1217,11 +1226,11 @@ integrity sha512-wtb+IbUIrIf8CrN6MLQuFR7nlU5C7PwuebfeEXfjthUha1+XZj2RVi+5k/lukToA24sZkYAiSJfHM8uG/UZIdg== "@csstools/postcss-is-pseudo-class@^4.0.3": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.5.tgz#c2b9a89e8c2f4cb80c3587dae1ed544447bbd16e" - integrity sha512-qG3MI7IN3KY9UwdaE9E7G7sFydscVW7nAj5OGwaBP9tQPEEVdxXTGI+l1ZW5EUpZFSj+u3q/22fH5+8HI72+Bg== + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-4.0.6.tgz#149b3bf9dde739932a545079da917ca25464cba0" + integrity sha512-HilOhAsMpFheMYkuaREZx+CGa4hsG6kQdzwXSsuqKDFzYz2eIMP213+3dH/vUbPXaWrzqLKr8m3i0dgYPoh7vg== dependencies: - "@csstools/selector-specificity" "^3.0.2" + "@csstools/selector-specificity" "^3.0.3" postcss-selector-parser "^6.0.13" "@csstools/postcss-logical-float-and-clear@^2.0.0": @@ -1247,31 +1256,31 @@ postcss-value-parser "^4.2.0" "@csstools/postcss-logical-viewport-units@^2.0.3": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.6.tgz#1f91e865e73f5d135038c519957a3b95ffe552ad" - integrity sha512-6hV0ngZh8J7HqNY3kyt+z5ABN/XE18qvrU7ne4YSkKfltrWDnQgGiW/Q+h7bdQz8/W5juAefcdCCAJUIBE7erg== + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-2.0.7.tgz#3bb03b9a57fe9ec2304bc35cf6c3d5d7c938ee26" + integrity sha512-L4G3zsp/bnU0+WXUyysihCUH14LkfMgUJsS9vKz3vCYbVobOTqQRoNXnEPpyNp8WYyolLqAWbGGJhVu8J6u2OQ== dependencies: - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/utilities" "^1.0.0" "@csstools/postcss-media-minmax@^1.1.0": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.3.tgz#87ff7af309916b36fe00e1f4ad6e03a5c16e74b9" - integrity sha512-W9AFRQSLvT+Dxtp20AewzGTUxzkJ21XSKzqRALwQdAv0uJGXkR76qgdhkoX0L/tcV4gXtgDfVtGYL/x2Nz/M5Q== + version "1.1.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.4.tgz#1af01cc02fdb936a1c10a11e2663fd1b1ce1bd79" + integrity sha512-xl/PIO3TUbXO1ZA4SA6HCw+Q9UGe2cgeRKx3lHCzoNig2D4bT5vfVCOrwhxjUb09oHihc9eI3I0iIfVPiXaN1A== dependencies: - "@csstools/css-calc" "^1.1.7" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/media-query-list-parser" "^2.1.8" + "@csstools/css-calc" "^1.2.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/media-query-list-parser" "^2.1.9" "@csstools/postcss-media-queries-aspect-ratio-number-values@^2.0.3": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.6.tgz#ca6dae6949bfb0f274a4029776614720e243acbe" - integrity sha512-awc2qenSDvx6r+w6G9xxENp+LsbvHC8mMMV23KYmk4pR3YL8JxeKPDSiDhmqd93FQ9nNNDc/CaCQEcvP+GV4rw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-2.0.7.tgz#5f4939e6330a3c2cd0cba1e1b76bc51a74dc839c" + integrity sha512-HBDAQw1K0NilcHGMUHv8jzf2mpOtcWTVKtuY3AeZ5TS1uyWWNVi5/yuA/tREPLU9WifNdqHQ+rfbsV/8zTIkTg== dependencies: - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/media-query-list-parser" "^2.1.8" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/media-query-list-parser" "^2.1.9" "@csstools/postcss-nested-calc@^3.0.0": version "3.0.2" @@ -1289,32 +1298,32 @@ postcss-value-parser "^4.2.0" "@csstools/postcss-oklab-function@^3.0.7": - version "3.0.10" - resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.10.tgz#9f230ce28a266de8a8e264025aebce41313d4053" - integrity sha512-s9trs1c+gUMtaTtwrrIpdVQkUbRuwi6bQ9rBHaqwt4kd3kEnEYfP85uLY1inFx6Rt8OM2XVg3PSYbfnFSAO51A== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "3.0.14" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-3.0.14.tgz#7d8b2e9c46c72f019962cfa375f46582c49281a5" + integrity sha512-92xdpcfc2wB3z4+GftPA0PXMuGI/tRLw9Tc0+HzpaAHHxyLK6aCJtoQIcw0Ox/PthXtqXZn/3wWT/Idfe8I7Wg== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" -"@csstools/postcss-progressive-custom-properties@^3.0.2", "@csstools/postcss-progressive-custom-properties@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.1.0.tgz#e4d6143b3ba50d1f7435932fd112db31e18f05af" - integrity sha512-Mfb1T1BHa6pktLI+poMEHI7Q+VYvAsdwJZPFsSkIB2ZUsawCiPxXLw06BKSVPITxFlaY/FEUzfpyOTfX9YCE2w== +"@csstools/postcss-progressive-custom-properties@^3.0.2", "@csstools/postcss-progressive-custom-properties@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-3.2.0.tgz#811da8616938e8148a7c4fb40c26e30bf94d4ceb" + integrity sha512-BZlirVxCRgKlE7yVme+Xvif72eTn1MYXj8oZ4Knb+jwaH4u3AN1DjbhM7j86RP5vvuAOexJ4JwfifYYKWMN/QQ== dependencies: postcss-value-parser "^4.2.0" "@csstools/postcss-relative-color-syntax@^2.0.7": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.10.tgz#07b9484c841623e32777bd7becac7679ce62c08d" - integrity sha512-IkTIk9Eq2VegSN4lgsljGY8boyfX3l3Pw58e+R9oyPe/Ye7r3NwuiQ3w0nkXoQ+RC+d240V6n7eZme2mEPqQvg== + version "2.0.14" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-2.0.14.tgz#a983c3c3d389905037776f36f7fb6611d4de6316" + integrity sha512-NlxgLjAjVCTUVGiWk8WNj3dKvux9eC6O5aLM3BmdA8UXEwBHYI9r4IqlanxG9PlcXnzhTUX6eZsqgmxwt4FPow== dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" "@csstools/postcss-scope-pseudo-class@^3.0.0": @@ -1325,40 +1334,45 @@ postcss-selector-parser "^6.0.13" "@csstools/postcss-stepped-value-functions@^3.0.2": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.5.tgz#857cf8eb6bb6ac2831cabe58c15604cfb95af1b2" - integrity sha512-B8K8RaTrYVZLxbNzVUvFO3SlCDJDaUTAO7KRth05fa7f01ufPvb6ztdBuxSoRwOtmNp8iROxPJHOemWo2kBBtA== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-3.0.6.tgz#8263ddafab483100e13d63929d43cd12fb14767f" + integrity sha512-rnyp8tWRuBXERTHVdB5hjUlif5dQgPcyN+BX55wUnYpZ3LN9QPfK2Z3/HUZymwyou8Gg6vhd6X2W+g1pLq1jYg== dependencies: - "@csstools/css-calc" "^1.1.7" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/css-calc" "^1.2.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/postcss-text-decoration-shorthand@^3.0.3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.4.tgz#b8c5216faa2c9d8a05b3f93da7b403dd5dd53a79" - integrity sha512-yUZmbnUemgQmja7SpOZeU45+P49wNEgQguRdyTktFkZsHf7Gof+ZIYfvF6Cm+LsU1PwSupy4yUeEKKjX5+k6cQ== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-3.0.6.tgz#108afc5a66b96db3d0cca4f5d9414559c6b7a0bf" + integrity sha512-Q8HEu4AEiwNVZBD6+DpQ8M9SajpMow4+WtmndWIAv8qxDtDYL4JK1xXWkhOGk28PrcJawOvkrEZ8Ri59UN1TJw== dependencies: - "@csstools/color-helpers" "^4.0.0" + "@csstools/color-helpers" "^4.2.0" postcss-value-parser "^4.2.0" "@csstools/postcss-trigonometric-functions@^3.0.2": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.5.tgz#bf9f061120bed802fe133188a94c82ba79c440d6" - integrity sha512-RhBfQ0TsBudyPuoo8pXKdfQuUiQxMU/Sc5GyV57bWk93JbUHXq6b4CdPx+B/tHUeFKvocVJn/e2jbu96rh0d3Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-3.0.6.tgz#f8227f1807d28e817e4ff9053093eb8f1bcd9e13" + integrity sha512-i5Zd0bMJooZAn+ZcDmPij2WCkcOJJJ6opzK+QeDjxbMrYmoGQl0CY8FDHdeQyBF1Nly+Q0Fq3S7QfdNLKBBaCg== dependencies: - "@csstools/css-calc" "^1.1.7" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/css-calc" "^1.2.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/postcss-unset-value@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-3.0.1.tgz#598a25630fd9ab0edf066d235916f7441404942a" integrity sha512-dbDnZ2ja2U8mbPP0Hvmt2RMEGBiF1H7oY6HYSpjteXJGihYwgxgTr6KRbbJ/V6c+4wd51M+9980qG4gKVn5ttg== -"@csstools/selector-specificity@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz#ea61ba7bb24be3502c6aaa3190ed231f4633a81e" - integrity sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg== +"@csstools/selector-resolve-nested@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz#d872f2da402d3ce8bd0cf16ea5f9fba76b18e430" + integrity sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg== + +"@csstools/selector-specificity@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.3.tgz#208a3929ee614967a1fc8cd6cb758d9fcbf0caae" + integrity sha512-KEPNw4+WW5AVEIyzC80rTbWEUatTW2lXpN8+8ILC8PiPeWPjwUzrPZDIOZ2wwqDmeqOYTdSGyL3+vE5GC3FB3Q== "@csstools/utilities@^1.0.0": version "1.0.0" @@ -1425,9 +1439,9 @@ integrity sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA== "@electron/asar@^3.2.1": - version "3.2.8" - resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.8.tgz#2ea722f3452583dbd4ffdcc4b4f5dc903f1d8178" - integrity sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg== + version "3.2.9" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.9.tgz#7b3a1fd677b485629f334dd80ced8c85353ba7e7" + integrity sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA== dependencies: commander "^5.0.0" glob "^7.1.6" @@ -1457,9 +1471,9 @@ fs-extra "^9.0.1" "@electron/osx-sign@^1.0.4": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.0.5.tgz#0af7149f2fce44d1a8215660fd25a9fb610454d8" - integrity sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww== + version "1.2.0" + resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.2.0.tgz#d1c83cac03e52f210858594eb011142a1ad0ae70" + integrity sha512-kOA3bAeDXFMj2JHj0R2fk/IT92qpu3tZHwM4l/PdksAuy7eA3/23QktCiAoQICwPdtxCYdt9ZLimKvnNyUpdSQ== dependencies: compare-version "^0.1.2" debug "^4.3.4" @@ -1581,9 +1595,9 @@ hoist-non-react-statics "^3.3.1" "@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.3.tgz#84b77bfcfe3b7bb47d326602f640ccfcacd5ffb0" - integrity sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA== + version "1.1.4" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451" + integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ== dependencies: "@emotion/hash" "^0.9.1" "@emotion/memoize" "^0.8.1" @@ -1631,6 +1645,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" @@ -1641,6 +1660,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + "@esbuild/android-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" @@ -1651,6 +1675,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + "@esbuild/android-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" @@ -1661,6 +1690,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" @@ -1671,6 +1705,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + "@esbuild/darwin-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" @@ -1681,6 +1720,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" @@ -1691,6 +1735,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + "@esbuild/freebsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" @@ -1701,6 +1750,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + "@esbuild/linux-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" @@ -1711,6 +1765,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + "@esbuild/linux-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" @@ -1721,6 +1780,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + "@esbuild/linux-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" @@ -1731,6 +1795,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + "@esbuild/linux-loong64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" @@ -1741,6 +1810,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + "@esbuild/linux-mips64el@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" @@ -1751,6 +1825,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + "@esbuild/linux-ppc64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" @@ -1761,6 +1840,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + "@esbuild/linux-riscv64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" @@ -1771,6 +1855,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + "@esbuild/linux-s390x@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" @@ -1781,6 +1870,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + "@esbuild/linux-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" @@ -1791,6 +1885,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + "@esbuild/netbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" @@ -1801,6 +1900,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + "@esbuild/openbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" @@ -1811,6 +1915,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + "@esbuild/sunos-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" @@ -1821,6 +1930,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + "@esbuild/win32-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" @@ -1831,6 +1945,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + "@esbuild/win32-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" @@ -1841,6 +1960,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + "@esbuild/win32-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" @@ -1851,6 +1975,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1999,9 +2128,9 @@ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@hutson/parse-repository-url@^3.0.0": version "3.0.2" @@ -2013,10 +2142,10 @@ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== -"@interactjs/types@1.10.26": - version "1.10.26" - resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.26.tgz#5a6c0ef1dda9763515ff1192a40ecc99101c7a48" - integrity sha512-DekYpdkMV3XJVd/0k3f4pJluZAsCiG86yEtVXvGLK0lS/Fj0+OzYEv7HoMpcBZSkQ8s7//yaeEBgnxy2tV81lA== +"@interactjs/types@1.10.27": + version "1.10.27" + resolved "https://registry.yarnpkg.com/@interactjs/types/-/types-1.10.27.tgz#10afd71cef2498e2b5192cf0d46f937d8ceb767f" + integrity sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -2096,7 +2225,7 @@ magic-string "^0.27.0" react-docgen-typescript "^2.2.2" -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== @@ -2116,19 +2245,19 @@ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -2177,13 +2306,6 @@ lodash "^4.17.15" tmp-promise "^3.0.2" -"@mapbox/hast-util-table-cell-style@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz#5b7166ae01297d72216932b245e4b2f0b642dca6" - integrity sha512-QsEsh5YaDvHoMQ2YHdvZy2iDnU3GgKVBTcHf6cILyoWDZtPSdlG444pL/ioPYO/GpXSfODBb9sefEetfC4v9oA== - dependencies: - unist-util-visit "^1.3.0" - "@mdx-js/react@^2.1.5": version "2.3.0" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3" @@ -2403,16 +2525,15 @@ react-hook-form "7.50.1" react-i18next "13.5.0" react-intersection-observer "^8.33.1" + react-markdown "9.0.1" react-redux "8.1.2" react-router-dom "5.3.4" react-select "5.4.0" - react-simple-keyboard "^3.4.187" + react-simple-keyboard "^3.7.0" react-viewport-list "6.3.0" redux "4.0.5" redux-observable "1.1.0" redux-thunk "2.3.0" - remark "9.0.0" - remark-react "4.0.3" reselect "4.0.0" rxjs "^6.5.1" semver "5.5.0" @@ -2918,70 +3039,85 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6" - integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w== - -"@rollup/rollup-android-arm64@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2" - integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ== - -"@rollup/rollup-darwin-arm64@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69" - integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ== - -"@rollup/rollup-darwin-x64@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8" - integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg== - -"@rollup/rollup-linux-arm-gnueabihf@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d" - integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA== - -"@rollup/rollup-linux-arm64-gnu@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68" - integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA== - -"@rollup/rollup-linux-arm64-musl@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7" - integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ== - -"@rollup/rollup-linux-riscv64-gnu@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7" - integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw== - -"@rollup/rollup-linux-x64-gnu@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3" - integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA== - -"@rollup/rollup-linux-x64-musl@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05" - integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw== - -"@rollup/rollup-win32-arm64-msvc@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e" - integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw== - -"@rollup/rollup-win32-ia32-msvc@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40" - integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA== - -"@rollup/rollup-win32-x64-msvc@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235" - integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg== +"@rollup/rollup-android-arm-eabi@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.1.tgz#ad76cc870b1e2bc4476dfc02b82e20cea272a09d" + integrity sha512-92/y0TqNLRYOTXpm6Z7mnpvKAG9P7qmK7yJeRJSdzElNCUnsgbpAsGqerUboYRIQKzgfq4pWu9xVkgpWLfmNsw== + +"@rollup/rollup-android-arm64@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.1.tgz#e7bd4f2b8ec5e049f98edbc68d72cb05356f81d8" + integrity sha512-ttWB6ZCfRLuDIUiE0yiu5gcqOsYjA5F7kEV1ggHMj20FwLZ8A1FMeahZJFl/pnOmcnD2QL0z4AcDuo27utGU8A== + +"@rollup/rollup-darwin-arm64@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.1.tgz#8fd277b4be6cc956167710e36b4ee365f8a44050" + integrity sha512-QLDvPLetbqjHojTGFw9+nuSP3YY/iz2k1cep6crYlr97sS+ZJ0W43b8Z0zC00+lnFZj6JSNxiA4DjboNQMuh1A== + +"@rollup/rollup-darwin-x64@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.1.tgz#5ab829322926fefce42db3529649a1098b420fe3" + integrity sha512-TAUK/D8khRrRIa1KwRzo8JNKk3tcqaeXWdtsiLgA8zmACWwlWLjPCJ4DULGHQrMkeBjp1Cd3Yuwx04lZgFx5Vg== + +"@rollup/rollup-linux-arm-gnueabihf@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.1.tgz#0154bc34e6a88fb0147adc827512add8d3a2338c" + integrity sha512-KO+WGZjrh6zyFTD1alIFkfdtxf8B4BC+hqd3kBZHscPLvE5FR/6QKsyuCT0JlERxxYBSUKNUQ/UHyX5uwO1x2A== + +"@rollup/rollup-linux-arm-musleabihf@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.1.tgz#0f3fa433a81b389042555133d38b4b886b369e58" + integrity sha512-NqxbllzIB1WoAo4ThUXVtd21iiM5IHMTTXmXySKBLVcZvkU0HIZmatlP7hLzb5yQubcmdIeWmncd2NdsjocEiw== + +"@rollup/rollup-linux-arm64-gnu@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.1.tgz#c8f2d523ac4bcff382601306989b27137d536dd6" + integrity sha512-snma5NvV8y7IECQ5rq0sr0f3UUu+92NVmG/913JXJMcXo84h9ak9TA5UI9Cl2XRM9j3m37QwDBtEYnJzRkSmxA== + +"@rollup/rollup-linux-arm64-musl@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.1.tgz#86b5104635131182b6b2b6997c4aa5594ce557b7" + integrity sha512-KOvqGprlD84ueivhCi2flvcUwDRD20mAsE3vxQNVEI2Di9tnPGAfEu6UcrSPZbM+jG2w1oSr43hrPo0RNg6GGg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.1.tgz#5de8b20105aaaeb36eb86fab0a1020d81c7bd4d5" + integrity sha512-/gsNwtiGLqYwN4vP+EIdUC6Q6LTlpupWqokqIndvZcjn9ig/5P01WyaYCU2wvfL/2Z82jp5kX8c1mDBOvCP3zg== + +"@rollup/rollup-linux-riscv64-gnu@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.1.tgz#5319629dcdcb85ba201c6f0f894c9472e7d1013d" + integrity sha512-uU8zuGkQfGqfD9w6VRJZI4IuG4JIfNxxJgEmLMAmPVHREKGsxFVfgHy5c6CexQF2vOfgjB33OsET3Vdn2lln9A== + +"@rollup/rollup-linux-s390x-gnu@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.1.tgz#246ac211ed0d78f7a9bc5c1d0653bde4c6cd9f63" + integrity sha512-lsjLtDgtcGFEuBP6yrXwkRN5/wKlvUZtfbKZZu0yaoNpiBL4epgnO21osAALIspVRnl4qZgyLFd8xjCYYWgwfw== + +"@rollup/rollup-linux-x64-gnu@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.1.tgz#d0c03203ddeb9454fc6fdde93a39b01c176ac6d9" + integrity sha512-N2ZizKhUryqqrMfdCnjhJhZRgv61C6gK+hwVtCIKC8ts8J+go+vqENnGexwg21nHIOvLN5mBM8a7DI2vlyIOPg== + +"@rollup/rollup-linux-x64-musl@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.1.tgz#20235632e2be4689d663aadaceaaf90df03b1a33" + integrity sha512-5ICeMxqg66FrOA2AbnBQ2TJVxfvZsKLxmof0ibvPLaYtbsJqnTUtJOofgWb46Gjd4uZcA4rdsp4JCxegzQPqCg== + +"@rollup/rollup-win32-arm64-msvc@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.1.tgz#af113ad682fc13d1f870242c5539031f8cc27cf1" + integrity sha512-1vIP6Ce02L+qWD7uZYRiFiuAJo3m9kARatWmFSnss0gZnVj2Id7OPUU9gm49JPGasgcR3xMqiH3fqBJ8t00yVg== + +"@rollup/rollup-win32-ia32-msvc@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.1.tgz#4e7b57e757c95da8e79092056d1b428617515668" + integrity sha512-Y3M92DcVsT6LoP+wrKpoUWPaazaP1fzbNkp0a0ZSj5Y//+pQVfVe/tQdsYQQy7dwXR30ZfALUIc9PCh9Izir6w== + +"@rollup/rollup-win32-x64-msvc@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.1.tgz#5068a893ba292279adbe76fc487316724b15d811" + integrity sha512-x0fvpHMuF7fK5r8oZxSi8VYXkrVmRgubXpO/wcf15Lk3xZ4Jvvh5oG+u7Su1776A7XzVKZhD2eRc4t7H50gL3w== "@serialport/binding-mock@10.2.2": version "10.2.2" @@ -3497,9 +3633,9 @@ lodash "^4.17.15" "@storybook/csf@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.2.tgz#8e7452f0097507f5841b5ade3f5da1525bc9afb2" - integrity sha512-ePrvE/pS1vsKR9Xr+o+YwdqNgHUyXvg+1Xjx0h9LrVx7Zq4zNe06pd63F5EvzTbCbJsHj7GHr9tkiaqm7U8WRA== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.4.tgz#18224bcd571fa834ccc4bebda8a0ca4cedbc4d91" + integrity sha512-B9UI/lsQMjF+oEfZCI6YXNoeuBcGZoOP5x8yKbe2tIEmsMjSztFKkpPzi5nLCnBk/MBtl6QJeI3ksJnbsWPkOw== dependencies: type-fest "^2.19.0" @@ -3856,7 +3992,7 @@ resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-3.0.1.tgz#98d747a2e5e9a56070c6bf14e27bff56204e34cc" integrity sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g== -"@types/debug@^4.1.6": +"@types/debug@^4.0.0", "@types/debug@^4.1.6": version "4.1.12" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== @@ -3893,6 +4029,13 @@ resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.6.tgz#5230a9ce796e042cda6f086dbf19f22ea330659c" integrity sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig== +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -3909,9 +4052,9 @@ integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@^4.17.33": - version "4.17.43" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" - integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== + version "4.19.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" + integrity sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ== dependencies: "@types/node" "*" "@types/qs" "*" @@ -3968,6 +4111,13 @@ dependencies: "@types/node" "*" +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" @@ -4035,25 +4185,27 @@ "@types/node" "*" "@types/lodash@^4.14.165", "@types/lodash@^4.14.167", "@types/lodash@^4.14.191": - version "4.14.202" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" - integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3" + integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA== + +"@types/mdast@^4.0.0": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" + integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== + dependencies: + "@types/unist" "*" "@types/mdx@^2.0.0": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.11.tgz#21f4c166ed0e0a3a733869ba04cd8daea9834b8e" - integrity sha512-HM5bwOaIQJIQbAYfax35HCKxx7a3KrK3nBtIqJgSOitivTD1y3oW9P3rxY9RkXYPUk7y/AjAohfHKmFpGE79zw== + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== "@types/mime-types@^2.1.0": version "2.1.4" resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== -"@types/mime@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" - integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw== - "@types/mime@^1": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" @@ -4100,16 +4252,16 @@ form-data "^4.0.0" "@types/node@*": - version "20.11.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792" - integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long== + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== dependencies: undici-types "~5.26.4" "@types/node@^18.0.0", "@types/node@^18.11.18": - version "18.19.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.21.tgz#f4ca1ac8ffb05ee4b89163c2d6fac9a1a59ee149" - integrity sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw== + version "18.19.31" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.31.tgz#b7d4a00f7cb826b60a543cebdbda5d189aaecdcd" + integrity sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA== dependencies: undici-types "~5.26.4" @@ -4137,9 +4289,9 @@ integrity sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA== "@types/prop-types@*": - version "15.7.11" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" - integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== "@types/pump@^1.1.0": version "1.1.3" @@ -4154,9 +4306,9 @@ integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== "@types/qs@*", "@types/qs@^6.9.5": - version "6.9.12" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.12.tgz#afa96b383a3a6fdc859453a1892d41b607fc7756" - integrity sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg== + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== "@types/query-string@6.2.0": version "6.2.0" @@ -4184,9 +4336,9 @@ "@types/react" "*" "@types/react-dom@^18.0.0": - version "18.2.19" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58" - integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA== + version "18.2.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" + integrity sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA== dependencies: "@types/react" "*" @@ -4225,12 +4377,11 @@ "@types/react" "*" "@types/react@*", "@types/react@>=16": - version "18.2.62" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.62.tgz#2527a7a54749b1a99c87a4aa8b83e26846face38" - integrity sha512-l3f57BbaEKP0xcFzf+5qRG8/PXykZiuVM6eEoPtqBPCp6dxO3HhDkLIgIyXPhPKNAeXn3KO2pEaNgzaEo/asaw== + version "18.2.79" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.79.tgz#c40efb4f255711f554d47b449f796d1c7756d865" + integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w== dependencies: "@types/prop-types" "*" - "@types/scheduler" "*" csstype "^3.0.2" "@types/react@18.2.51": @@ -4281,9 +4432,9 @@ "@types/node" "*" "@types/scheduler@*": - version "0.16.8" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" - integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + version "0.23.0" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.23.0.tgz#0a6655b3e2708eaabca00b7372fafd7a792a7b09" + integrity sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw== "@types/semver@^6.0.1": version "6.2.7" @@ -4304,13 +4455,13 @@ "@types/node" "*" "@types/serve-static@*": - version "1.15.5" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" - integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== dependencies: "@types/http-errors" "*" - "@types/mime" "*" "@types/node" "*" + "@types/send" "*" "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" @@ -4372,9 +4523,9 @@ integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== "@types/verror@^1.10.3": - version "1.10.9" - resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.9.tgz#420c32adb9a2dd50b3db4c8f96501e05a0e72941" - integrity sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ== + version "1.10.10" + resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087" + integrity sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg== "@types/vfile-message@*": version "2.0.0" @@ -4573,7 +4724,7 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== @@ -5202,9 +5353,9 @@ argparse@^2.0.1: integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-hidden@^1.1.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" - integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== dependencies: tslib "^2.0.0" @@ -5266,14 +5417,15 @@ array-ify@^1.0.0: integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== array-includes@^3.1.6, array-includes@^3.1.7: - version "3.1.7" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" - integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" is-string "^1.0.7" array-union@^1.0.1, array-union@^1.0.2: @@ -5298,37 +5450,28 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== -array.prototype.filter@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz#423771edeb417ff5914111fff4277ea0624c0d0e" - integrity sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - array.prototype.findlast@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz#eeb9e45fc894055c82e5675c463e8077b827ad36" - integrity sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw== + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" + es-abstract "^1.23.2" es-errors "^1.3.0" + es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" array.prototype.findlastindex@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz#d1c50f0b3a9da191981ff8942a0aedd82794404f" - integrity sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" + es-abstract "^1.23.2" es-errors "^1.3.0" + es-object-atoms "^1.0.0" es-shim-unscopables "^1.0.2" array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: @@ -5352,14 +5495,16 @@ array.prototype.flatmap@^1.3.2: es-shim-unscopables "^1.0.0" array.prototype.reduce@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.6.tgz#63149931808c5fc1e1354814923d92d45f7d96d5" - integrity sha512-UW+Mz8LG/sPSU8jRDCjVr6J/ZKAGpHfwrZ6kWTG5qCxIEiXdVshqGnu5vEZA8S1y6X4aCSbQZ0/EEsfvEvBiSg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz#6aadc2f995af29cb887eb866d981dc85ab6f7dc7" + integrity sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" es-array-method-boxes-properly "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" is-string "^1.0.7" array.prototype.toreversed@^1.1.2: @@ -5407,15 +5552,14 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== +asn1.js@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== dependencies: bn.js "^4.0.0" inherits "^2.0.1" minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" asn1@~0.2.3: version "0.2.6" @@ -5512,13 +5656,6 @@ async@^3.2.0, async@^3.2.2, async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== -asynciterator.prototype@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" - integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== - dependencies: - has-symbols "^1.0.3" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -5535,12 +5672,12 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^10.0.2, autoprefixer@^10.4, autoprefixer@^10.4.16: - version "10.4.18" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.18.tgz#fcb171a3b017be7cb5d8b7a825f5aacbf2045163" - integrity sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g== + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== dependencies: browserslist "^4.23.0" - caniuse-lite "^1.0.30001591" + caniuse-lite "^1.0.30001599" fraction.js "^4.3.7" normalize-range "^0.1.2" picocolors "^1.0.0" @@ -5559,7 +5696,7 @@ autoprefixer@^9.5.1: postcss "^7.0.32" postcss-value-parser "^4.1.0" -available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: +available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== @@ -5567,9 +5704,9 @@ available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: possible-typed-array-names "^1.0.0" aws-sdk@^2.264.1, aws-sdk@^2.493.0: - version "2.1569.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1569.0.tgz#b7568698ae4172be543536cfb9399361ac9955d0" - integrity sha512-9puKjesHKOjAYPqFurW/9nv3qhQ+STu3bVa5PN158SCeZPE6NsxZIWnHLglJvKU7N8UXJo1aJHmKDUGrsS7rXw== + version "2.1603.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1603.0.tgz#81fd7e76502d46acf06c726a3dc547bdee9d3e5e" + integrity sha512-AB54LyTODyiTKdtUzE0LzhnPbumYH3KhDstje8i4KfeBSGCeszTgq89YfOVpSy5twfu9WpsJ2cHOxlrlXn9cpg== dependencies: buffer "4.9.2" events "1.1.1" @@ -5634,29 +5771,29 @@ babel-plugin-macros@^3.1.0: cosmiconfig "^7.0.0" resolve "^1.19.0" -babel-plugin-polyfill-corejs2@^0.4.8: - version "0.4.8" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz#dbcc3c8ca758a290d47c3c6a490d59429b0d2269" - integrity sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg== +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.10" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" + integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.5.0" + "@babel/helper-define-polyfill-provider" "^0.6.1" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz#9eea32349d94556c2ad3ab9b82ebb27d4bf04a81" - integrity sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg== +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.5.0" - core-js-compat "^3.34.0" + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" -babel-plugin-polyfill-regenerator@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz#8b0c8fc6434239e5d7b8a9d1f832bb2b0310f06a" - integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg== +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz#4f08ef4c62c7a7f66a35ed4c0d75e30506acc6be" + integrity sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g== dependencies: - "@babel/helper-define-polyfill-provider" "^0.5.0" + "@babel/helper-define-polyfill-provider" "^0.6.1" babel-plugin-styled-components@2.0.7: version "2.0.7" @@ -5703,6 +5840,11 @@ bail@^1.0.0: resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -5781,9 +5923,9 @@ binary-extensions@^1.0.0: integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bindings@^1.5.0: version "1.5.0" @@ -5976,7 +6118,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserify-aes@^1.0.0, browserify-aes@^1.0.4: +browserify-aes@^1.0.4, browserify-aes@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== @@ -6016,18 +6158,19 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" - integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== + version "4.2.3" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" + integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== dependencies: bn.js "^5.2.1" browserify-rsa "^4.1.0" create-hash "^1.2.0" create-hmac "^1.1.7" - elliptic "^6.5.4" + elliptic "^6.5.5" + hash-base "~3.0" inherits "^2.0.4" - parse-asn1 "^5.1.6" - readable-stream "^3.6.2" + parse-asn1 "^5.1.7" + readable-stream "^2.3.8" safe-buffer "^5.2.1" browserify-zlib@^0.1.4: @@ -6044,7 +6187,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.22.3, browserslist@^4.23.0: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -6201,9 +6344,9 @@ builtin-status-codes@^3.0.0: integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== builtins@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" - integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== + version "5.1.0" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.1.0.tgz#6d85eeb360c4ebc166c3fdef922a15aa7316a5e8" + integrity sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg== dependencies: semver "^7.0.0" @@ -6441,10 +6584,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591: - version "1.0.30001593" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz#7cda1d9e5b0cad6ebab4133b1f239d4ea44fe659" - integrity sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: + version "1.0.30001612" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz#d34248b4ec1f117b70b24ad9ee04c90e0b8a14ae" + integrity sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g== caseless@~0.12.0: version "0.12.0" @@ -6456,6 +6599,11 @@ ccount@^1.0.0, ccount@^1.0.3: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chai@^4.3.10: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" @@ -6510,21 +6658,41 @@ character-entities-html4@^1.0.0: resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" integrity sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g== +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + character-entities-legacy@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + character-entities@^1.0.0: version "1.2.4" resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + character-reference-invalid@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -6626,7 +6794,7 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -citty@^0.1.5, citty@^0.1.6: +citty@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== @@ -6679,16 +6847,7 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== -cli-table3@^0.6.1: - version "0.6.3" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -cli-table3@~0.6.1: +cli-table3@^0.6.1, cli-table3@~0.6.1: version "0.6.4" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.4.tgz#d1c536b8a3f2e7bec58f67ac9e5769b1b30088b0" integrity sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw== @@ -6785,7 +6944,7 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" -collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: +collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== @@ -6878,6 +7037,11 @@ comma-separated-tokens@^1.0.0, comma-separated-tokens@^1.0.1: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@2.17.x: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -7015,6 +7179,11 @@ conf@^6.2.1: semver "^6.2.0" write-file-atomic "^3.0.0" +confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + config-file-ts@^0.2.4: version "0.2.6" resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.6.tgz#b424ff74612fb37f626d6528f08f92ddf5d22027" @@ -7250,10 +7419,10 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-concurrently@^1.0.0: version "1.0.5" @@ -7272,17 +7441,17 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== -core-js-compat@^3.31.0, core-js-compat@^3.34.0: - version "3.36.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.0.tgz#087679119bc2fdbdefad0d45d8e5d307d45ba190" - integrity sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw== +core-js-compat@^3.31.0, core-js-compat@^3.36.1: + version "3.37.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73" + integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA== dependencies: - browserslist "^4.22.3" + browserslist "^4.23.0" core-js-pure@^3.30.2: - version "3.36.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.36.0.tgz#ffb34330b14e594d6a9835cf5843b4123f1d95db" - integrity sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ== + version "3.37.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.0.tgz#ce99fb4a7cec023fdbbe5b5bd1f06bbcba83316e" + integrity sha512-d3BrpyFr5eD4KcbRvQ3FTUx/KWmaDesr7+a3+1+P46IUnNoEt+oiLijPINZMEon7w9oGkIINWxrBAU9DEciwFQ== core-js@3.2.1: version "3.2.1" @@ -7300,9 +7469,9 @@ core-js@^2.4.0: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.6.4: - version "3.36.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.0.tgz#e752fa0b0b462a0787d56e9d73f80b0f7c0dde68" - integrity sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw== + version "3.37.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb" + integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug== core-util-is@1.0.2: version "1.0.2" @@ -7424,9 +7593,9 @@ crypto-random-string@^2.0.0: integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== css-blank-pseudo@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-6.0.1.tgz#f79f8b84cc00f891e16aa85f14093c5e1c3499a8" - integrity sha512-goSnEITByxTzU4Oh5oJZrEWudxTqk7L6IXj1UW69pO6Hv0UdX+Vsrt02FFu5DweRh2bLu6WpX/+zsQCu5O1gKw== + version "6.0.2" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-6.0.2.tgz#50db072d4fb5b40c2df9ffe5ca5fbb9b19c77fc8" + integrity sha512-J/6m+lsqpKPqWHOifAFtKFeGLOzw3jR92rxQcwRUfA/eTuZzKfKlxOmYDx2+tqOPQAueNvBiY8WhAeHu5qNmTg== dependencies: postcss-selector-parser "^6.0.13" @@ -7449,11 +7618,11 @@ css-declaration-sorter@^4.0.1: timsort "^0.3.0" css-has-pseudo@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-6.0.2.tgz#a1a15ee7082d72a23ed1d810220ba384da867d15" - integrity sha512-Z2Qm5yyOvJRTy6THdUlnGIX6PW/1wOc4FHWlfkcBkfkpZ3oz6lPdG+h+J7t1HZHT4uSSVR8XatXiMpqMUADXow== + version "6.0.3" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-6.0.3.tgz#babd8f208507d553b3986ee803b3adf4dd09c00e" + integrity sha512-qIsDxK/z0byH/mpNsv5hzQ5NOl8m1FRmOLgZpx4bG5uYHnOlO2XafeMI4mFIgNSViHwoUWcxSJZyyijaAmbs+A== dependencies: - "@csstools/selector-specificity" "^3.0.2" + "@csstools/selector-specificity" "^3.0.3" postcss-selector-parser "^6.0.13" postcss-value-parser "^4.2.0" @@ -7579,9 +7748,9 @@ css.escape@^1.5.1: integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== cssdb@^7.9.0: - version "7.11.1" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.1.tgz#491841b281d337d7e5332e43b282429dd241b377" - integrity sha512-F0nEoX/Rv8ENTHsjMPGHd9opdjGfXkgRBafSUGnQKPzGZFB7Lm0BbT10x21TMOCrKLbVsJ0NoCDMk6AfKqw8/A== + version "7.11.2" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== cssesc@^3.0.0: version "3.0.0" @@ -7789,6 +7958,33 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.10.0.tgz#abd10604d8bafb0bcbd2ba2e9b0563b922ae4b6b" @@ -7869,6 +8065,13 @@ decimal.js@^10.2.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -8044,7 +8247,7 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-data-property@^1.0.1, define-data-property@^1.1.2, define-data-property@^1.1.4: +define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== @@ -8089,7 +8292,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defu@^6.1.3: +defu@^6.1.4: version "6.1.4" resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== @@ -8157,7 +8360,7 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.0, dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -8180,13 +8383,6 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== -detab@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43" - integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g== - dependencies: - repeat-string "^1.5.4" - detect-file@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" @@ -8198,9 +8394,9 @@ detect-indent@^6.1.0: integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== detect-libc@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" - integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== detect-node-es@^1.1.0: version "1.1.0" @@ -8302,6 +8498,13 @@ detective-typescript@^5.8.0: node-source-walk "^4.2.0" typescript "^3.8.3" +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + diagnostics@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a" @@ -8610,14 +8813,14 @@ duplexify@^3.4.2, duplexify@^3.5.0, duplexify@^3.6.0: stream-shift "^1.0.0" duplexify@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" - integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw== + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== dependencies: end-of-stream "^1.4.1" inherits "^2.0.3" readable-stream "^3.1.1" - stream-shift "^1.0.0" + stream-shift "^1.0.2" eastasianwidth@^0.2.0: version "0.2.0" @@ -8643,9 +8846,9 @@ ejs@^2.6.1: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== ejs@^3.1.8: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" @@ -8783,9 +8986,9 @@ electron-store@5.1.1: type-fest "^0.7.1" electron-to-chromium@^1.4.668: - version "1.4.690" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz#dd5145d45c49c08a9a6f7454127e660bdf9a3fa7" - integrity sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA== + version "1.4.745" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290" + integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA== electron-updater@4.1.2: version "4.1.2" @@ -8810,10 +9013,10 @@ electron@27.0.0: "@types/node" "^18.11.18" extract-zip "^2.0.1" -elliptic@^6.5.3, elliptic@^6.5.4: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== +elliptic@^6.5.3, elliptic@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded" + integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -8921,9 +9124,9 @@ env-variable@0.0.x: integrity sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg== envinfo@^7.7.3: - version "7.11.1" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.1.tgz#2ffef77591057081b0129a8fd8cf6118da1b94e1" - integrity sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.12.0.tgz#b56723b39c2053d67ea5714f026d05d4f5cc7acd" + integrity sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg== err-code@^2.0.2: version "2.0.3" @@ -8944,17 +9147,21 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22.4: - version "1.22.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.5.tgz#1417df4e97cc55f09bf7e58d1e614bc61cb8df46" - integrity sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w== +es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== dependencies: array-buffer-byte-length "^1.0.1" arraybuffer.prototype.slice "^1.0.3" available-typed-arrays "^1.0.7" call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" es-define-property "^1.0.0" es-errors "^1.3.0" + es-object-atoms "^1.0.0" es-set-tostringtag "^2.0.3" es-to-primitive "^1.2.1" function.prototype.name "^1.1.6" @@ -8965,10 +9172,11 @@ es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22 has-property-descriptors "^1.0.2" has-proto "^1.0.3" has-symbols "^1.0.3" - hasown "^2.0.1" + hasown "^2.0.2" internal-slot "^1.0.7" is-array-buffer "^3.0.4" is-callable "^1.2.7" + is-data-view "^1.0.1" is-negative-zero "^2.0.3" is-regex "^1.1.4" is-shared-array-buffer "^1.0.3" @@ -8979,17 +9187,17 @@ es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.22 object-keys "^1.1.1" object.assign "^4.1.5" regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.0" + safe-array-concat "^1.1.2" safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.8" - string.prototype.trimend "^1.0.7" - string.prototype.trimstart "^1.0.7" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" typed-array-buffer "^1.0.2" typed-array-byte-length "^1.0.1" typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.5" + typed-array-length "^1.0.6" unbox-primitive "^1.0.2" - which-typed-array "^1.1.14" + which-typed-array "^1.1.15" es-array-method-boxes-properly@^1.0.0: version "1.0.0" @@ -9003,7 +9211,7 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.0.0, es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== @@ -9024,32 +9232,38 @@ es-get-iterator@^1.1.3: stop-iteration-iterator "^1.0.0" es-iterator-helpers@^1.0.17: - version "1.0.17" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz#123d1315780df15b34eb181022da43e734388bb8" - integrity sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ== + version "1.0.18" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" + integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== dependencies: - asynciterator.prototype "^1.0.0" call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.4" + es-abstract "^1.23.0" es-errors "^1.3.0" - es-set-tostringtag "^2.0.2" + es-set-tostringtag "^2.0.3" function-bind "^1.1.2" get-intrinsic "^1.2.4" globalthis "^1.0.3" has-property-descriptors "^1.0.2" - has-proto "^1.0.1" + has-proto "^1.0.3" has-symbols "^1.0.3" internal-slot "^1.0.7" iterator.prototype "^1.1.2" - safe-array-concat "^1.1.0" + safe-array-concat "^1.1.2" es-module-lexer@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== -es-set-tostringtag@^2.0.2, es-set-tostringtag@^2.0.3: +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== @@ -9160,6 +9374,35 @@ esbuild@^0.19.3: "@esbuild/win32-ia32" "0.19.12" "@esbuild/win32-x64" "0.19.12" +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -9196,10 +9439,12 @@ escodegen@^2.0.0, escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-compat-utils@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz#f45e3b5ced4c746c127cf724fb074cd4e730d653" - integrity sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg== +eslint-compat-utils@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.0.tgz#f7b2eb2befec25a370fac76934d3f9189f312a65" + integrity sha512-dc6Y8tzEcSYZMHa+CMPLi/hyo1FzNeonbhJL7Ol0ccuKQkwopJcJBA9YL/xmMTLU1eKigXo9vj9nALElWYSowg== + dependencies: + semver "^7.5.4" eslint-config-prettier@^8.1.0: version "8.10.0" @@ -9241,20 +9486,20 @@ eslint-module-utils@^2.8.0: debug "^3.2.7" eslint-plugin-cypress@^2.11.2: - version "2.15.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.1.tgz#336afa7e8e27451afaf65aa359c9509e0a4f3a7b" - integrity sha512-eLHLWP5Q+I4j2AWepYq0PgFEei9/s5LvjuSqWrxurkg1YZ8ltxdvMNmdSf0drnsNo57CTgYY/NIHHLRSWejR7w== + version "2.15.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.2.tgz#f22e12fad4c434edad7b298ef92bac8fa087ffa0" + integrity sha512-CtcFEQTDKyftpI22FVGpx8bkpKyYXBlNge6zSo0pl5/qJvBAnzaD76Vu2AsP16d6mTj478Ldn2mhgrWV+Xr0vQ== dependencies: globals "^13.20.0" eslint-plugin-es-x@^7.5.0: - version "7.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz#d08d9cd155383e35156c48f736eb06561d07ba92" - integrity sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ== + version "7.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-es-x/-/eslint-plugin-es-x-7.6.0.tgz#ccee7a4556c0f816d1ae88fd0eea21540e8ccd65" + integrity sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA== dependencies: "@eslint-community/eslint-utils" "^4.1.2" "@eslint-community/regexpp" "^4.6.0" - eslint-compat-utils "^0.1.2" + eslint-compat-utils "^0.5.0" eslint-plugin-import@^2.29.1: version "2.29.1" @@ -9315,14 +9560,14 @@ eslint-plugin-react-hooks@^4.6.0: integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react-refresh@^0.4.4: - version "0.4.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.5.tgz#6b9b307bad3feba2244ef64a1a15485ac70a2d0f" - integrity sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w== + version "0.4.6" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz#e8e8accab681861baed00c5c12da70267db0936f" + integrity sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA== eslint-plugin-react@^7.22.0: - version "7.34.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz#ab71484d54fc409c37025c5eca00eb4177a5e88c" - integrity sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ== + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== dependencies: array-includes "^3.1.7" array.prototype.findlast "^1.2.4" @@ -9359,9 +9604,9 @@ eslint-plugin-storybook@^0.8.0: ts-dedent "^2.2.0" eslint-plugin-testing-library@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz#af3340b783c881eb19ec5ac6b3a4bfe8ab4a1f74" - integrity sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw== + version "6.2.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.2.tgz#67e84ff891a2b3a8078ced0afa95ee6f343c00c1" + integrity sha512-1E94YOTUDnOjSLyvOwmbVDzQi/WkKm3WVrMXu6SmBr6DN95xTGZmI6HJ/eOkSXh/DlheRsxaPsJvZByDBhWLVQ== dependencies: "@typescript-eslint/utils" "^5.58.0" @@ -9481,6 +9726,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + estree-walker@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" @@ -9700,16 +9950,16 @@ express@4.16.4: vary "~1.1.2" express@^4.16.3, express@^4.16.4, express@^4.17.1, express@^4.17.3: - version "4.18.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4" - integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -10199,9 +10449,9 @@ flatten@^1.0.2: integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== flow-parser@0.*: - version "0.229.2" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.229.2.tgz#b19ce67bfbfab8c91ee51dcddb9c3ab0f3bf2ab7" - integrity sha512-T72XV2Izvl7yV6dhHhLaJ630Y6vOZJl6dnOS6dN0bPW9ExuREu7xGAf3omtcxX76POTuux9TJPu9ZpS48a/rdw== + version "0.234.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.234.0.tgz#92af26f40ea7e79ca4bd66a066d6d6aa3b4223bf" + integrity sha512-J1Wn32xDF1l8FqwshoQnTwC9K3aJ83MFuXUx9AcBQr8ttbI/rkjEgAqnjxaIJuZ6RGMfccN5ZxDJSOMM64qy9Q== flush-write-stream@^1.0.0: version "1.1.1" @@ -10212,9 +10462,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-each@^0.3.3: version "0.3.3" @@ -10490,7 +10740,7 @@ get-func-name@^2.0.1, get-func-name@^2.0.2: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -10595,9 +10845,9 @@ get-symbol-description@^1.0.2: get-intrinsic "^1.2.4" get-tsconfig@^4.7.0: - version "4.7.2" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce" - integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== + version "4.7.3" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.3.tgz#0498163d98f7b58484dd4906999c0c9d5f103f83" + integrity sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg== dependencies: resolve-pkg-maps "^1.0.0" @@ -10621,17 +10871,17 @@ getpass@^0.1.1: assert-plus "^1.0.0" giget@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.1.tgz#4f42779aae57a5f664a1c4d50401b008e9810f4c" - integrity sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g== + version "1.2.3" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.3.tgz#ef6845d1140e89adad595f7f3bb60aa31c672cb6" + integrity sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA== dependencies: - citty "^0.1.5" + citty "^0.1.6" consola "^3.2.3" - defu "^6.1.3" - node-fetch-native "^1.6.1" - nypm "^0.3.3" + defu "^6.1.4" + node-fetch-native "^1.6.3" + nypm "^0.3.8" ohash "^1.1.3" - pathe "^1.1.1" + pathe "^1.1.2" tar "^6.2.0" git-raw-commits@^2.0.8: @@ -10713,15 +10963,15 @@ glob-to-regexp@^0.4.1: integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@^10.0.0, glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" + jackspeak "^2.3.6" minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + minipass "^7.0.4" + path-scurry "^1.10.2" glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.3" @@ -11044,7 +11294,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1, has-property-descriptors@^1.0.2: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== @@ -11073,7 +11323,7 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" -has-tostringtag@^1.0.0, has-tostringtag@^1.0.1, has-tostringtag@^1.0.2: +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== @@ -11130,6 +11380,14 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +hash-base@~3.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -11138,26 +11396,13 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hasown@^2.0.0, hasown@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" - integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" -hast-to-hyperscript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-4.0.0.tgz#3eb25483ec72a8e9a71e4b1ad7eb8f7c86f755db" - integrity sha512-4kOn4ihjDJTQg7B53ZcZ6NyExtTeG3hLNZv6rSKhq4haQvD52zCllE+49iLiC1VWuc4DbHmt96FHPGlHbslZqQ== - dependencies: - comma-separated-tokens "^1.0.0" - is-nan "^1.2.1" - kebab-case "^1.0.0" - property-information "^3.0.0" - space-separated-tokens "^1.0.0" - trim "0.0.1" - unist-util-is "^2.0.0" - hast-util-from-parse5@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c" @@ -11184,13 +11429,6 @@ hast-util-parse-selector@^2.0.0: resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== -hast-util-sanitize@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-1.3.1.tgz#4e60d66336bd67e52354d581967467029a933f2e" - integrity sha512-AIeKHuHx0Wk45nSkGVa2/ujQYTksnDl8gmmKo/mwQi7ag7IBZ8cM3nJ2G86SajbjGP/HRpud6kMkPtcM2i0Tlw== - dependencies: - xtend "^4.0.1" - hast-util-to-html@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-6.1.0.tgz#86bcd19c3bd46af456984f8f34db16298c2b10b0" @@ -11207,11 +11445,39 @@ hast-util-to-html@^6.0.0: unist-util-is "^3.0.0" xtend "^4.0.1" +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz#3ed27caf8dc175080117706bf7269404a0aa4f7c" + integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + hast-util-whitespace@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.4.tgz#e4fe77c4a9ae1cb2e6c25e02df0043d0164f6e41" integrity sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A== +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + hastscript@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" @@ -11372,6 +11638,11 @@ html-tags@^3.0.0, html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-url-attributes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.0.tgz#fc4abf0c3fb437e2329c678b80abb3c62cff6f08" + integrity sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow== + html-void-elements@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" @@ -11744,12 +12015,17 @@ ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" + integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== + interactjs@^1.10.17: - version "1.10.26" - resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.26.tgz#ad009a46ee3610cb75de6aec22ea6cc0b0e277e2" - integrity sha512-5gNTNDTfEHp2EifqtWGi5VkD3CMZVJSTGmtK/IsVRd+rkOk3E63iVs5Z+IeD5K1Lr0qZpU2754VHAwf5i+Z9xg== + version "1.10.27" + resolved "https://registry.yarnpkg.com/interactjs/-/interactjs-1.10.27.tgz#16499aba4987a5ccfdaddca7d1ba7bb1118e14d0" + integrity sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA== dependencies: - "@interactjs/types" "1.10.26" + "@interactjs/types" "1.10.27" internal-ip@^4.3.0: version "4.3.0" @@ -11759,7 +12035,7 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -internal-slot@^1.0.4, internal-slot@^1.0.5, internal-slot@^1.0.7: +internal-slot@^1.0.4, internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== @@ -11843,6 +12119,11 @@ is-alphabetical@^1.0.0: resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + is-alphanumeric@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" @@ -11856,6 +12137,14 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" @@ -11918,7 +12207,7 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-buffer@^1.1.4, is-buffer@^1.1.5: +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -11980,6 +12269,13 @@ is-data-descriptor@^1.0.1: dependencies: hasown "^2.0.0" +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -11992,6 +12288,11 @@ is-decimal@^1.0.0, is-decimal@^1.0.2: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-deflate@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" @@ -12088,6 +12389,11 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -12113,17 +12419,17 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== -is-map@^2.0.1, is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== -is-nan@^1.2.1, is-nan@^1.3.2: +is-nan@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== @@ -12209,6 +12515,11 @@ is-plain-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@5.0.0, is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -12266,10 +12577,10 @@ is-retry-allowed@^1.1.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== -is-set@^2.0.1, is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: version "1.0.3" @@ -12336,10 +12647,10 @@ is-url@^1.2.4: resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2: version "1.0.2" @@ -12348,13 +12659,13 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" is-whitespace-character@^1.0.0: version "1.0.4" @@ -12499,7 +12810,7 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" -jackspeak@^2.3.5: +jackspeak@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== @@ -12757,7 +13068,7 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@^3.0.0, jsonc-parser@^3.2.0: +jsonc-parser@^3.0.0: version "3.2.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== @@ -12833,11 +13144,6 @@ jszip@^3.1.0: readable-stream "~2.3.6" setimmediate "^1.0.5" -kebab-case@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/kebab-case/-/kebab-case-1.0.2.tgz#5eac97d5d220acf606d40e3c0ecfea21f1f9e1eb" - integrity sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q== - keyboardevent-from-electron-accelerator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-2.0.0.tgz#ace21b1aa4e47148815d160057f9edb66567c50c" @@ -13185,6 +13491,11 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -13240,6 +13551,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -13259,11 +13575,6 @@ lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -13321,20 +13632,20 @@ magic-string@^0.27.0: "@jridgewell/sourcemap-codec" "^1.4.13" magic-string@^0.30.0, magic-string@^0.30.5: - version "0.30.8" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" - integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" magicast@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.3.tgz#a15760f982deec9dabc5f314e318d7c6bddcb27b" - integrity sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw== + version "0.3.4" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.4.tgz#bbda1791d03190a24b00ff3dd18151e7fd381d19" + integrity sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q== dependencies: - "@babel/parser" "^7.23.6" - "@babel/types" "^7.23.6" - source-map-js "^1.0.2" + "@babel/parser" "^7.24.4" + "@babel/types" "^7.24.0" + source-map-js "^1.2.0" make-dir@^1.0.0: version "1.3.0" @@ -13437,9 +13748,9 @@ markdown-table@^1.1.0: integrity sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q== markdown-to-jsx@^7.1.8: - version "7.4.1" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.1.tgz#1ed6a60f8f9cd944bec39d9923fbbc8d3d60dcb9" - integrity sha512-GbrbkTnHp9u6+HqbPRFJbObi369AgJNXi/sGqq5HRsoZW063xR1XDCaConqq+whfEIAlzB1YPnOgsPc7B7bc/A== + version "7.4.7" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz#740ee7ec933865ef5cc683a0992797685a75e2ee" + integrity sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg== match-sorter@^6.0.2: version "6.3.4" @@ -13482,13 +13793,6 @@ mdast-util-compact@^1.0.0: dependencies: unist-util-visit "^1.1.0" -mdast-util-definitions@^1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.5.tgz#3fe622a4171c774ebd06f11e9f8af7ec53ea5c74" - integrity sha512-CJXEdoLfiISCDc2JB6QLb79pYfI6+GcIH+W2ox9nMc7od0Pz+bovcHsiq29xAQY6ayqe/9CsK2VzkSJdg1pFYA== - dependencies: - unist-util-visit "^1.0.0" - mdast-util-definitions@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" @@ -13496,28 +13800,116 @@ mdast-util-definitions@^4.0.0: dependencies: unist-util-visit "^2.0.0" -mdast-util-to-hast@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz#132001b266031192348d3366a6b011f28e54dc40" - integrity sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA== +mdast-util-from-markdown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== dependencies: - collapse-white-space "^1.0.0" - detab "^2.0.0" - mdast-util-definitions "^1.2.0" - mdurl "^1.0.1" - trim "0.0.1" - trim-lines "^1.0.0" - unist-builder "^1.0.1" - unist-util-generated "^1.1.0" - unist-util-position "^3.0.0" - unist-util-visit "^1.1.0" - xtend "^4.0.1" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" + integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz#daae777c72f9c4a106592e3025aa50fb26068e1b" + integrity sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^5.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124" + integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" mdast-util-to-string@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -13542,11 +13934,6 @@ mdns-js@1.0.1: dns-js "~0.2.1" semver "^5.4.1" -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -13639,6 +14026,200 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3" + integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" + integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz#76129c49ac65da6e479c09d0ec4b5f29ec6eace5" + integrity sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + +micromark@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -13763,7 +14344,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@9.0.3, minimatch@^9.0, minimatch@^9.0.1: +minimatch@9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== @@ -13784,6 +14365,13 @@ minimatch@^5.0.1, minimatch@^5.1.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0, minimatch@^9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -13862,7 +14450,7 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: version "7.0.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== @@ -13933,7 +14521,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mlly@^1.2.0, mlly@^1.4.2: +mlly@^1.4.2, mlly@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.6.1.tgz#0983067dc3366d6314fc5e12712884e6978d028f" integrity sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA== @@ -14067,9 +14655,9 @@ mutexify@^1.1.0: queue-tick "^1.0.0" nan@^2.12.1: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== + version "2.19.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" + integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== nano-time@1.0.0: version "1.0.0" @@ -14148,9 +14736,9 @@ no-case@^2.2.0: lower-case "^1.1.1" node-abi@^3.0.0, node-abi@^3.45.0: - version "3.56.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.56.0.tgz#ca807d5ff735ac6bbbd684ae3ff2debc1c2a40a7" - integrity sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q== + version "3.60.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.60.0.tgz#a325b13b3c401c2230202897559fbf0b5f9a90ac" + integrity sha512-zcGgwoXbzw9NczqbGzAWL/ToDYAxv1V8gL1D67ClbdkIfeeDBbY0GelZtC25ayLvVjr2q2cloHeQV1R0QAWqRQ== dependencies: semver "^7.3.5" @@ -14195,10 +14783,10 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" -node-fetch-native@^1.6.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.2.tgz#f439000d972eb0c8a741b65dcda412322955e1c6" - integrity sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w== +node-fetch-native@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== node-fetch@2.6.7: version "2.6.7" @@ -14442,11 +15030,11 @@ number-allocator@^1.0.9: js-sdsl "4.3.0" nwsapi@^2.2.0: - version "2.2.7" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.7.tgz#738e0707d3128cb750dddcfe90e4610482df0f30" - integrity sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ== + version "2.2.9" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.9.tgz#7f3303218372db2e9f27c27766bcfc59ae7e61c6" + integrity sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg== -nypm@^0.3.3: +nypm@^0.3.8: version "0.3.8" resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.8.tgz#a16b078b161be5885351e72cf0b97326973722bf" integrity sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og== @@ -14512,52 +15100,54 @@ object.assign@^4.1.4, object.assign@^4.1.5: object-keys "^1.1.1" object.entries@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" - integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" object.fromentries@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" - integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: - version "2.1.7" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz#7a466a356cd7da4ba8b9e94ff6d35c3eeab5d56a" - integrity sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g== + version "2.1.8" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz#2f1fe0606ec1a7658154ccd4f728504f69667923" + integrity sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A== dependencies: array.prototype.reduce "^1.0.6" - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - safe-array-concat "^1.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + gopd "^1.0.1" + safe-array-concat "^1.1.2" object.groupby@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.2.tgz#494800ff5bab78fd0eff2835ec859066e00192ec" - integrity sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw== + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - array.prototype.filter "^1.0.3" - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.0.0" + es-abstract "^1.23.2" object.hasown@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" - integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== dependencies: - define-properties "^1.2.0" - es-abstract "^1.22.1" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" object.pick@^1.3.0: version "1.3.0" @@ -14567,13 +15157,13 @@ object.pick@^1.3.0: isobject "^3.0.1" object.values@^1.1.0, object.values@^1.1.6, object.values@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" - integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" objectorarray@^1.0.5: version "1.0.5" @@ -14865,16 +15455,17 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== +parse-asn1@^5.0.0, parse-asn1@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" + integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" + asn1.js "^4.10.1" + browserify-aes "^1.2.0" + evp_bytestokey "^1.0.3" + hash-base "~3.0" + pbkdf2 "^3.1.2" + safe-buffer "^5.2.1" parse-entities@^1.0.2, parse-entities@^1.1.0: version "1.2.2" @@ -14888,6 +15479,20 @@ parse-entities@^1.0.2, parse-entities@^1.1.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" +parse-entities@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + dependencies: + "@types/unist" "^2.0.0" + character-entities "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -14993,12 +15598,12 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.7: @@ -15030,7 +15635,7 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pathe@^1.1.0, pathe@^1.1.1, pathe@^1.1.2: +pathe@^1.1.1, pathe@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== @@ -15040,7 +15645,7 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pbkdf2@^3.0.3: +pbkdf2@^3.0.3, pbkdf2@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== @@ -15139,13 +15744,13 @@ pkg-dir@^5.0.0: find-up "^5.0.0" pkg-types@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" - integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + version "1.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.0.tgz#3ec1bf33379030fd0a34c227b6c650e8ea7ca271" + integrity sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA== dependencies: - jsonc-parser "^3.2.0" - mlly "^1.2.0" - pathe "^1.1.0" + confbox "^0.1.7" + mlly "^1.6.1" + pathe "^1.1.2" pkg-up@^3.0.1: version "3.1.0" @@ -15231,14 +15836,14 @@ postcss-clamp@^4.1.0: postcss-value-parser "^4.2.0" postcss-color-functional-notation@^6.0.2: - version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.5.tgz#eca158e833b5655c5715c998e92aab9481124c18" - integrity sha512-aTFsIy89ftjyclwUHRwvz1IxucLzVrzmmcXmtbPWT9GdyYeaJEKeAwbaZzOZn7AQlXg4xfwgkYhKsofC4aLIwg== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "6.0.9" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-6.0.9.tgz#30b7a2f4f4fc0f7ccdcb720ea8d311eebb4a260b" + integrity sha512-8i/ofOArZ4fljp+3g+HI6Pok01Kb8YaSqInrJt2vMimEKrI0ZDNRLpH+wLhXBNu/Bi8zeWDvxhvCqsGSpu8E6Q== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" postcss-color-hex-alpha@^9.0.2: @@ -15286,34 +15891,34 @@ postcss-convert-values@^4.0.1: postcss-value-parser "^3.0.0" postcss-custom-media@^10.0, postcss-custom-media@^10.0.2: - version "10.0.3" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-10.0.3.tgz#7131ee7f6e55cbb0423dcfca37c8946539f1b214" - integrity sha512-wfJ9nKpLn/Qy7LASKu0Rj9Iq2uMzlRt27P4FAE1889IKRMdYUgy8SqvdXfAOs7LJLQX9Fjm0mZ+TSFphD/mKwA== + version "10.0.4" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-10.0.4.tgz#f40fcf05f3ee95e7a34bbdcb4dff99da41f0238f" + integrity sha512-Ubs7O3wj2prghaKRa68VHBvuy3KnTQ0zbGwqDYY1mntxJD0QL2AeiAy+AMfl3HBedTCVr2IcFNktwty9YpSskA== dependencies: - "@csstools/cascade-layer-name-parser" "^1.0.8" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/media-query-list-parser" "^2.1.8" + "@csstools/cascade-layer-name-parser" "^1.0.9" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/media-query-list-parser" "^2.1.9" postcss-custom-properties@^13.3.2: - version "13.3.5" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-13.3.5.tgz#0083841407dbf93c833457ecffdf1a3d74a76d10" - integrity sha512-xHg8DTCMfN2nrqs2CQTF+0m5jgnzKL5zrW5Y05KF6xBRO0uDPxiplBm/xcr1o49SLbyJXkMuaRJKhRzkrquKnQ== + version "13.3.7" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-13.3.7.tgz#87f8ce173b147dc5e7e7d7f36123042b2572afd6" + integrity sha512-0N9F/GUCr/D0IazjzHahyYW2bQVDT6qDtEudiGHAhMd3XqhfM3VmfYVlkc/40DOhsPtngSNb54/Ctu8msvFOvQ== dependencies: - "@csstools/cascade-layer-name-parser" "^1.0.8" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/cascade-layer-name-parser" "^1.0.9" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" "@csstools/utilities" "^1.0.0" postcss-value-parser "^4.2.0" postcss-custom-selectors@^7.1.6: - version "7.1.7" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-7.1.7.tgz#66b7adb9a3470ba11860ad7847947c7fd29e985d" - integrity sha512-N19MpExaR+hYTXU59VO02xE42zLoAUYSVcupwkKlWWLteOb+sWCWHw5FhV7u7gVLTzaGULy7nZP3DNTHgOZAPA== + version "7.1.8" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-7.1.8.tgz#600ce32a487737038831bb679e9a8011ffc62ee5" + integrity sha512-fqDkGSEsO7+oQaqdRdR8nwwqH+N2uk6LE/2g4myVJJYz/Ly418lHKEleKTdV/GzjBjFcG4n0dbfuH/Pd2BE8YA== dependencies: - "@csstools/cascade-layer-name-parser" "^1.0.8" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" + "@csstools/cascade-layer-name-parser" "^1.0.9" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" postcss-selector-parser "^6.0.13" postcss-dir-pseudo-class@^8.0.0: @@ -15352,11 +15957,11 @@ postcss-discard-overridden@^4.0.1: postcss "^7.0.0" postcss-double-position-gradients@^5.0.2: - version "5.0.4" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.4.tgz#294787043e5e6187b5489ee52950ecfb303f9ea9" - integrity sha512-xOH2QhazCPeYR+ziYaDcGlpo7Bpw8PVoggOFfU/xPkmBRUQH8MR2eWoPY1CZM93CB0WKs2mxq3ORo83QGIooLw== + version "5.0.6" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-5.0.6.tgz#fec69371a131b67ec92740bcf8c9ad6ce7f168d3" + integrity sha512-QJ+089FKMaqDxOhhIHsJrh4IP7h4PIHNC5jZP5PMmnfUScNu8Hji2lskqpFWCvu+5sj+2EJFyzKd13sLEWOZmQ== dependencies: - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" postcss-value-parser "^4.2.0" @@ -15425,14 +16030,14 @@ postcss-jsx@^0.36.3: "@babel/core" ">=7.2.2" postcss-lab-function@^6.0.7: - version "6.0.10" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-6.0.10.tgz#efe1bbf9fa1f1034890a0ad078286bfbace11106" - integrity sha512-Csvw/CwwuwTojK2O3Ad0SvYKrfnAKy+uvT+1Fjk6igR+n8gHuJHIwdj1A2s46EZZojg3RkibdMBuv1vMvR6Sng== - dependencies: - "@csstools/css-color-parser" "^1.5.2" - "@csstools/css-parser-algorithms" "^2.6.0" - "@csstools/css-tokenizer" "^2.2.3" - "@csstools/postcss-progressive-custom-properties" "^3.1.0" + version "6.0.14" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-6.0.14.tgz#ccc4dec33e01cae3a435b45de2f29b455286ea10" + integrity sha512-ddQS9FRWT8sfl4wfW0ae8fpP2JdLIuhC9pYpHq1077avjrLzg73T9IEVu5QmFa72nJhYFlO9CbqjcoSdEzfY9A== + dependencies: + "@csstools/css-color-parser" "^2.0.0" + "@csstools/css-parser-algorithms" "^2.6.1" + "@csstools/css-tokenizer" "^2.2.4" + "@csstools/postcss-progressive-custom-properties" "^3.2.0" "@csstools/utilities" "^1.0.0" postcss-less@^3.1.4: @@ -15569,11 +16174,12 @@ postcss-modules-values@^3.0.0: postcss "^7.0.6" postcss-nesting@^12.0, postcss-nesting@^12.0.1: - version "12.0.4" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-12.0.4.tgz#593d577fd1fbbfbe0997a6c81dbff074b26c83a2" - integrity sha512-WuCe0KnP4vKjLZK8VNoUWKL8ZLOv/5jiM94mHcI3VszLropHwmjotdUyP/ObzqZpXuQKP2Jf9R12vIHKFSStKw== + version "12.1.2" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-12.1.2.tgz#e7aba3f73b86a0e944e84798d481b54dcfce802e" + integrity sha512-FUmTHGDNundodutB4PUBxt/EPuhgtpk8FJGRsBhOuy+6FnkR2A8RZWIsyyy6XmhvX2DZQQWIkvu+HB4IbJm+Ew== dependencies: - "@csstools/selector-specificity" "^3.0.2" + "@csstools/selector-resolve-nested" "^1.1.0" + "@csstools/selector-specificity" "^3.0.3" postcss-selector-parser "^6.0.13" postcss-normalize-charset@^4.0.1: @@ -15757,9 +16363,9 @@ postcss-preset-env@9.3.0: postcss-value-parser "^4.2.0" postcss-pseudo-class-any-link@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.1.tgz#71c24a886765763d4e37e21a27ecc6f1c1a5d698" - integrity sha512-cKYGGZ9yzUZi+dZd7XT2M8iSDfo+T2Ctbpiizf89uBTBfIpZpjvTavzIJXpCReMVXSKROqzpxClNu6fz4DHM0Q== + version "9.0.2" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-9.0.2.tgz#e436a7db1421f8a347fff3f19951a27d4e791987" + integrity sha512-HFSsxIqQ9nA27ahyfH37cRWGk3SYyQLpk0LiWw/UGMV4VKT5YG2ONee4Pz/oFesnK0dn2AjcyequDbIjKJgB0g== dependencies: postcss-selector-parser "^6.0.13" @@ -15842,9 +16448,9 @@ postcss-selector-parser@^3.0.0, postcss-selector-parser@^3.1.0: uniq "^1.0.1" postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2: - version "6.0.15" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" - integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" + integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -15917,14 +16523,14 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21 picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.1.7, postcss@^8.4, postcss@^8.4.32, postcss@^8.4.35: - version "8.4.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" - integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== +postcss@^8.1.7, postcss@^8.4, postcss@^8.4.32, postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" - source-map-js "^1.0.2" + source-map-js "^1.2.0" precinct@^6.3.1: version "6.3.1" @@ -16070,11 +16676,6 @@ property-expr@^2.0.4, property-expr@^2.0.5: resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== -property-information@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-3.2.0.tgz#fd1483c8fbac61808f5fe359e7693a1f48a58331" - integrity sha512-BKU45RMZAA+3npkQ/VxEH7EeZImQcfV6rfKH0O4HkkDz3uqqz+689dbkjiWia00vK390MY6EARPS6TzNS4tXPg== - property-information@^5.0.0, property-information@^5.2.0: version "5.6.0" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -16082,6 +16683,11 @@ property-information@^5.0.0, property-information@^5.2.0: dependencies: xtend "^4.0.0" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + proxy-addr@~2.0.4, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -16229,11 +16835,11 @@ qs@6.5.2: integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== qs@^6.10.0, qs@^6.11.2: - version "6.11.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" - integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + version "6.12.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" + integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" qs@~6.5.2: version "6.5.3" @@ -16502,6 +17108,22 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-markdown@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1" + integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + react-popper@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.0.0.tgz#b99452144e8fe4acc77fa3d959a8c79e07a65084" @@ -16541,9 +17163,9 @@ react-refresh@^0.14.0: integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== react-remove-scroll-bar@^2.1.0, react-remove-scroll-bar@^2.3.3: - version "2.3.5" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c" - integrity sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw== + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== dependencies: react-style-singleton "^2.2.1" tslib "^2.0.0" @@ -16611,10 +17233,10 @@ react-select@5.4.0: prop-types "^15.6.0" react-transition-group "^4.3.0" -react-simple-keyboard@^3.4.187: - version "3.7.93" - resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.93.tgz#2343be2f96d59ab1f00ce8dcd0ed576eb9f59945" - integrity sha512-MJSwiBOiU0xMjyHfrHVJ6YJkH/TKga4S4DINfqL+MbNYglJ0qMhCyLxorjjlqs744X71/+InV5Dnc8dYK7YMYg== +react-simple-keyboard@^3.7.0: + version "3.7.114" + resolved "https://registry.yarnpkg.com/react-simple-keyboard/-/react-simple-keyboard-3.7.114.tgz#7cd09474d90ed92d4ee05b462e160dd9cf16a13c" + integrity sha512-4CxIzJJx3sHqYqLZdjXmKR6LcGaYC4SEa/rWd2u5PxEJsG5adw4KWjhKSQvxUZDWMhL3Fe+WDMW02O9JcWawiA== react-snap@^1.23.0: version "1.23.0" @@ -16732,7 +17354,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -16755,7 +17377,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0, readable-stream@^3.6.2: +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -16781,9 +17403,9 @@ readdirp@~3.6.0: picomatch "^2.2.1" recast@^0.23.1, recast@^0.23.3: - version "0.23.5" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.5.tgz#07f5594a0d36e7754356160b70e90393cca0406d" - integrity sha512-M67zIddJiwXdfPQRYKJ0qZO1SLdH1I0hYeb0wzxA+pNOvAZiQHulWzuk+fYsEWRQ8VfZrgjyucqsCOtCyM01/A== + version "0.23.6" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.6.tgz#198fba74f66143a30acc81929302d214ce4e3bfa" + integrity sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ== dependencies: ast-types "^0.16.1" esprima "~4.0.0" @@ -16862,15 +17484,15 @@ redux@^4.0.0, redux@^4.0.5, redux@^4.2.0: "@babel/runtime" "^7.9.2" reflect.getprototypeof@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz#e0bd28b597518f16edaf9c0e292c631eb13e0674" - integrity sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.0.0" - get-intrinsic "^1.2.3" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" globalthis "^1.0.3" which-builtin-type "^1.1.3" @@ -16911,7 +17533,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== @@ -16996,26 +17618,15 @@ remark-external-links@^8.0.0: space-separated-tokens "^1.0.0" unist-util-visit "^2.0.0" -remark-parse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95" - integrity sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA== +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== dependencies: - collapse-white-space "^1.0.2" - is-alphabetical "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - is-word-character "^1.0.0" - markdown-escapes "^1.0.0" - parse-entities "^1.1.0" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - trim "0.0.1" - trim-trailing-lines "^1.0.0" - unherit "^1.0.4" - unist-util-remove-position "^1.0.0" - vfile-location "^2.0.0" - xtend "^4.0.1" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" remark-parse@^6.0.0: version "6.0.3" @@ -17038,15 +17649,16 @@ remark-parse@^6.0.0: vfile-location "^2.0.0" xtend "^4.0.1" -remark-react@4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/remark-react/-/remark-react-4.0.3.tgz#980938f3bcc93bef220215b26b0b0a80f3158c7d" - integrity sha512-M2DxXfX8/GK0hV84PUcsvkvb+8yGLdV+krb8mW28eoa9ZgTrhC5rk01EPRMXRNGCAEl3JMDFs+VKdT/FbsN9vg== +remark-rehype@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.0.tgz#d5f264f42bcbd4d300f030975609d01a1697ccdc" + integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== dependencies: - "@mapbox/hast-util-table-cell-style" "^0.1.3" - hast-to-hyperscript "^4.0.0" - hast-util-sanitize "^1.0.0" - mdast-util-to-hast "^3.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" remark-slug@^6.0.0: version "6.1.0" @@ -17057,26 +17669,6 @@ remark-slug@^6.0.0: mdast-util-to-string "^1.0.0" unist-util-visit "^2.0.0" -remark-stringify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-5.0.0.tgz#336d3a4d4a6a3390d933eeba62e8de4bd280afba" - integrity sha512-Ws5MdA69ftqQ/yhRF9XhVV29mhxbfGhbz0Rx5bQH+oJcNhhSM6nCu1EpLod+DjrFGrU0BMPs+czVmJZU7xiS7w== - dependencies: - ccount "^1.0.0" - is-alphanumeric "^1.0.0" - is-decimal "^1.0.0" - is-whitespace-character "^1.0.0" - longest-streak "^2.0.1" - markdown-escapes "^1.0.0" - markdown-table "^1.1.0" - mdast-util-compact "^1.0.0" - parse-entities "^1.0.2" - repeat-string "^1.5.4" - state-toggle "^1.0.0" - stringify-entities "^1.0.1" - unherit "^1.0.4" - xtend "^4.0.1" - remark-stringify@^6.0.0: version "6.0.4" resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088" @@ -17097,15 +17689,6 @@ remark-stringify@^6.0.0: unherit "^1.0.4" xtend "^4.0.1" -remark@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/remark/-/remark-9.0.0.tgz#c5cfa8ec535c73a67c4b0f12bfdbd3a67d8b2f60" - integrity sha512-amw8rGdD5lHbMEakiEsllmkdBP+/KpjW/PRK6NSGPZKCQowh0BT4IWXDAkRMyG3SB9dKPXWMviFjNusXzXNn3A== - dependencies: - remark-parse "^5.0.0" - remark-stringify "^5.0.0" - unified "^6.0.0" - remark@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/remark/-/remark-10.0.1.tgz#3058076dc41781bf505d8978c291485fe47667df" @@ -17444,26 +18027,29 @@ rollup@^2.44.0: optionalDependencies: fsevents "~2.3.2" -rollup@^4.2.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5" - integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q== +rollup@^4.13.0, rollup@^4.2.0: + version "4.16.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.16.1.tgz#5a60230987fe95ebe68bab517297c116dbb1a88d" + integrity sha512-5CaD3MPDlPKfhqzRvWXK96G6ELJfPZNb3LHiZxTHgDdC6jvwfGz2E8nY+9g1ONk4ttHsK1WaFP19Js4PSr1E3g== dependencies: "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.12.0" - "@rollup/rollup-android-arm64" "4.12.0" - "@rollup/rollup-darwin-arm64" "4.12.0" - "@rollup/rollup-darwin-x64" "4.12.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.12.0" - "@rollup/rollup-linux-arm64-gnu" "4.12.0" - "@rollup/rollup-linux-arm64-musl" "4.12.0" - "@rollup/rollup-linux-riscv64-gnu" "4.12.0" - "@rollup/rollup-linux-x64-gnu" "4.12.0" - "@rollup/rollup-linux-x64-musl" "4.12.0" - "@rollup/rollup-win32-arm64-msvc" "4.12.0" - "@rollup/rollup-win32-ia32-msvc" "4.12.0" - "@rollup/rollup-win32-x64-msvc" "4.12.0" + "@rollup/rollup-android-arm-eabi" "4.16.1" + "@rollup/rollup-android-arm64" "4.16.1" + "@rollup/rollup-darwin-arm64" "4.16.1" + "@rollup/rollup-darwin-x64" "4.16.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.16.1" + "@rollup/rollup-linux-arm-musleabihf" "4.16.1" + "@rollup/rollup-linux-arm64-gnu" "4.16.1" + "@rollup/rollup-linux-arm64-musl" "4.16.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.16.1" + "@rollup/rollup-linux-riscv64-gnu" "4.16.1" + "@rollup/rollup-linux-s390x-gnu" "4.16.1" + "@rollup/rollup-linux-x64-gnu" "4.16.1" + "@rollup/rollup-linux-x64-musl" "4.16.1" + "@rollup/rollup-win32-arm64-msvc" "4.16.1" + "@rollup/rollup-win32-ia32-msvc" "4.16.1" + "@rollup/rollup-win32-x64-msvc" "4.16.1" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -17494,13 +18080,13 @@ rxjs@^7.5.1, rxjs@^7.8.1: dependencies: tslib "^2.1.0" -safe-array-concat@^1.0.0, safe-array-concat@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" - integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - call-bind "^1.0.5" - get-intrinsic "^1.2.2" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" has-symbols "^1.0.3" isarray "^2.0.5" @@ -17800,18 +18386,18 @@ set-blocking@^2.0.0: integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== set-function-length@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" - integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - define-data-property "^1.1.2" + define-data-property "^1.1.4" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.3" + get-intrinsic "^1.2.4" gopd "^1.0.1" - has-property-descriptors "^1.0.1" + has-property-descriptors "^1.0.2" -set-function-name@^2.0.0, set-function-name@^2.0.1: +set-function-name@^2.0.1, set-function-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== @@ -17917,7 +18503,7 @@ shx@^0.3.3: minimist "^1.2.3" shelljs "^0.8.5" -side-channel@^1.0.4: +side-channel@^1.0.4, side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== @@ -17943,9 +18529,9 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-git@^3.15.1: - version "3.22.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.22.0.tgz#616d41c661e30f9c65778956317d422b1729a242" - integrity sha512-6JujwSs0ac82jkGjMHiCnTifvf1crOiY/+tfs/Pqih6iow7VrpNKRRNdWm6RtaXpvvv/JGNYhlUtLhGFqHF+Yw== + version "3.24.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.24.0.tgz#33a8c88dc6fa74e53eaf3d6bfc27d0182a49ec00" + integrity sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw== dependencies: "@kwsites/file-exists" "^1.1.1" "@kwsites/promise-deferred" "^1.1.1" @@ -18072,9 +18658,9 @@ socks-proxy-agent@^7.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.8.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.1.tgz#22c7d9dd7882649043cba0eafb49ae144e3457af" - integrity sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: ip-address "^9.0.5" smart-buffer "^4.2.0" @@ -18105,10 +18691,10 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== source-map-resolve@^0.5.0: version "0.5.3" @@ -18163,6 +18749,11 @@ space-separated-tokens@^1.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + spawn-command@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" @@ -18372,10 +18963,10 @@ store2@^2.14.2: resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5" integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg== -storybook-addon-pseudo-states@^1.15.5: - version "1.15.5" - resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-1.15.5.tgz#47d40391440dff235c05938c5b033aa655dda38e" - integrity sha512-DVngZ4121lJ6s42vKNfmLCBKhBMhh01D7sCV/LohP0rZoVW6Zws552g906Wan5R14gnArAlPCxQ+zbgm7QqxDA== +storybook-addon-pseudo-states@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-2.0.0.tgz#4fa251aaea04ebc6d17b7e57e5f09ea240f14583" + integrity sha512-tLuuwB1k+xFsX8C1fn4G/vJm5wX33jvSLeqTsJgWwI3/AKJUf6Thbg/kg14I2AwN8nqffjun2PzE05Iea23n0w== storybook@^7.6.16: version "7.6.17" @@ -18411,7 +19002,7 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-shift@^1.0.0: +stream-shift@^1.0.0, stream-shift@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== @@ -18459,46 +19050,50 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" string.prototype.matchall@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" - integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - get-intrinsic "^1.2.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.5" - regexp.prototype.flags "^1.5.0" - set-function-name "^2.0.0" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" -string.prototype.trim@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" - integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" -string.prototype.trimend@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" - integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -string.prototype.trimstart@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" - integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" @@ -18540,6 +19135,14 @@ stringify-entities@^2.0.0: is-decimal "^1.0.2" is-hexadecimal "^1.0.0" +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + stringify-object@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -18660,6 +19263,13 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== +style-to-object@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.6.tgz#0c28aed8be1813d166c60d962719b2907c26547b" + integrity sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA== + dependencies: + inline-style-parser "0.2.3" + styled-components@5.3.6: version "5.3.6" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.6.tgz#27753c8c27c650bee9358e343fc927966bfd00d1" @@ -18920,9 +19530,9 @@ tar-stream@^2.1.4: readable-stream "^3.1.1" tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2, tar@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" - integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -19014,9 +19624,9 @@ terser@^4.1.2, terser@^4.6.12: source-map-support "~0.5.12" terser@^5.0.0: - version "5.28.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.28.1.tgz#bf00f7537fd3a798c352c2d67d67d65c915d1b28" - integrity sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA== + version "5.30.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.4.tgz#62b4d16a819424e6317fd5ceffb4ee8dc769803a" + integrity sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -19110,9 +19720,9 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2: integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== tinybench@^2.5.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" - integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== + version "2.8.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" + integrity sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw== tinycolor2@^1.4.1: version "1.6.0" @@ -19120,9 +19730,9 @@ tinycolor2@^1.4.1: integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== tinypool@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.2.tgz#84013b03dc69dacb322563a475d4c0a9be00f82a" - integrity sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ== + version "0.8.4" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" + integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== tinyspy@^2.2.0: version "2.2.1" @@ -19194,9 +19804,9 @@ to-regex@3.0.2, to-regex@^3.0.1, to-regex@^3.0.2: safe-regex "^1.1.0" tocbot@^4.20.1: - version "4.25.0" - resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.25.0.tgz#bc38aea5ec8f076779bb39636f431b044129a237" - integrity sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA== + version "4.27.0" + resolved "https://registry.yarnpkg.com/tocbot/-/tocbot-4.27.0.tgz#8215d50df66dc6e3b86c8f7b10389b2d6b8bd555" + integrity sha512-x3ZPNFPVOYCAyW4CEW8KszGfqB3/fnY1QX1tfUHH1fj1r6I8v0g5w0flNsWf7htZKtzqtdiPqu//II3ngL/WwA== toidentifier@1.0.1: version "1.0.1" @@ -19253,10 +19863,10 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -trim-lines@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.3.tgz#839514be82428fd9e7ec89e35081afe8f6f93115" - integrity sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA== +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== trim-newlines@^2.0.0: version "2.0.0" @@ -19295,6 +19905,11 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + truncate-utf8-bytes@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" @@ -19308,9 +19923,9 @@ tryer@^1.0.1: integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== ts-api-utils@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" - integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== ts-dedent@^2.0.0, ts-dedent@^2.2.0: version "2.2.0" @@ -19458,10 +20073,10 @@ typed-array-byte-offset@^1.0.2: has-proto "^1.0.3" is-typed-array "^1.1.13" -typed-array-length@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.5.tgz#57d44da160296d8663fd63180a1802ebf25905d5" - integrity sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== dependencies: call-bind "^1.0.7" for-each "^0.3.3" @@ -19492,7 +20107,7 @@ typeface-open-sans@0.0.75: resolved "https://registry.yarnpkg.com/typeface-open-sans/-/typeface-open-sans-0.0.75.tgz#20d0c330f14c0c40463c334adbedd6005389abe4" integrity sha512-0lLmB7pfj113OP4T78SbpSmC4OCdFQ0vUxdSXQccsSb6qF76F92iEuC/DghFgmPswTyidk8+Hwf+PS/htiJoRQ== -typescript@5.3.3, typescript@^5.3.3: +typescript@5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== @@ -19502,15 +20117,20 @@ typescript@^3.0.3, typescript@^3.8.3, typescript@^3.9.5, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== +typescript@^5.3.3: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + ua-parser-js@^0.7.23, ua-parser-js@^0.7.30: version "0.7.37" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== ufo@^1.3.2, ufo@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32" - integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ== + version "1.5.3" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" + integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== uglify-js@3.4.x: version "3.4.10" @@ -19579,17 +20199,18 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unified@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba" - integrity sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA== +unified@^11.0.0: + version "11.0.4" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" + integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== dependencies: - bail "^1.0.0" + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" extend "^3.0.0" - is-plain-obj "^1.1.0" - trough "^1.0.0" - vfile "^2.0.0" - x-is-string "^0.1.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" unified@^7.0.0: version "7.1.0" @@ -19671,13 +20292,6 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -unist-builder@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-1.0.4.tgz#e1808aed30bd72adc3607f25afecebef4dd59e17" - integrity sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg== - dependencies: - object-assign "^4.1.0" - unist-util-find-all-after@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.5.tgz#5751a8608834f41d117ad9c577770c5f2f1b2899" @@ -19685,16 +20299,6 @@ unist-util-find-all-after@^1.0.2: dependencies: unist-util-is "^3.0.0" -unist-util-generated@^1.1.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" - integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== - -unist-util-is@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.3.tgz#459182db31f4742fceaea88d429693cbf0043d20" - integrity sha512-4WbQX2iwfr/+PfM4U3zd2VNXY+dWtZsN1fLnWEi2QQXA4qyDYAZcDMfXUX0Cu6XZUHHAO9q4nyxxLT4Awk1qUA== - unist-util-is@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd" @@ -19705,10 +20309,19 @@ unist-util-is@^4.0.0: resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797" integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== -unist-util-position@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" - integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" unist-util-remove-position@^1.0.0: version "1.1.4" @@ -19717,6 +20330,14 @@ unist-util-remove-position@^1.0.0: dependencies: unist-util-visit "^1.1.0" +unist-util-remove-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== + dependencies: + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6" @@ -19751,7 +20372,15 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" -unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0: +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^1.1.0, unist-util-visit@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3" integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw== @@ -19767,6 +20396,15 @@ unist-util-visit@^2.0.0: unist-util-is "^4.0.0" unist-util-visit-parents "^3.0.0" +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universal-user-agent@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" @@ -19801,9 +20439,9 @@ unpipe@1.0.0, unpipe@~1.0.0: integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== unplugin@^1.3.1: - version "1.8.0" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.8.0.tgz#08540733c6e2f2fe343735816d1f622b4d083dd1" - integrity sha512-yGEQsodWICmgt7asHF7QzqDZYeEP9h14vyd9Lul98UnYf29pLZZLwI09z2QdTjwU/FCkum1SRvsK7cx232X8NA== + version "1.10.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.10.1.tgz#8ceda065dc71bc67d923dea0920f05c67f2cd68c" + integrity sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg== dependencies: acorn "^8.11.3" chokidar "^3.6.0" @@ -19921,18 +20559,18 @@ url@^0.11.0: qs "^6.11.2" usb@^2.11.0: - version "2.12.0" - resolved "https://registry.yarnpkg.com/usb/-/usb-2.12.0.tgz#3c00faf6d3bf830b5a45fa510157f23df10e7d73" - integrity sha512-C/egt5PQWcBZq5jABOpBCbhZrB2ftyXdx+cEnK7qowo0ALkfclfrQGlCMbj0VbirfIGayvmWMYQ8Dnii5A4pXQ== + version "2.12.1" + resolved "https://registry.yarnpkg.com/usb/-/usb-2.12.1.tgz#f7a68ddb1314d56758e3e3b1265bc467922a9503" + integrity sha512-hgtoSQUFuMXVJBApelpUTiX7ZB83MQCbYeHTBsHftA2JG7YZ76ycwIgKQhkhKqVY76C8K6xJscHpF7Ep0eG3pQ== dependencies: "@types/w3c-web-usb" "^1.0.6" node-addon-api "^7.0.0" node-gyp-build "^4.5.0" use-callback-ref@^1.2.3, use-callback-ref@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0" - integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== dependencies: tslib "^2.0.0" @@ -20119,7 +20757,7 @@ vfile-location@^2.0.0: resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e" integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA== -vfile-message@*: +vfile-message@*, vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== @@ -20142,16 +20780,6 @@ vfile-message@^2.0.0: "@types/unist" "^2.0.0" unist-util-stringify-position "^2.0.0" -vfile@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a" - integrity sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w== - dependencies: - is-buffer "^1.1.4" - replace-ext "1.0.0" - unist-util-stringify-position "^1.0.0" - vfile-message "^1.0.0" - vfile@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/vfile/-/vfile-3.0.1.tgz#47331d2abe3282424f4a4bb6acd20a44c4121803" @@ -20172,6 +20800,15 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vfile@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + vite-node@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.2.2.tgz#f6d329b06f9032130ae6eac1dc773f3663903c25" @@ -20195,13 +20832,13 @@ vite@5.0.5: fsevents "~2.3.3" vite@^5.0, vite@^5.0.0: - version "5.1.4" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.4.tgz#14e9d3e7a6e488f36284ef13cebe149f060bcfb6" - integrity sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg== + version "5.2.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.10.tgz#2ac927c91e99d51b376a5c73c0e4b059705f5bd7" + integrity sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw== dependencies: - esbuild "^0.19.3" - postcss "^8.4.35" - rollup "^4.2.0" + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" optionalDependencies: fsevents "~2.3.3" @@ -20353,9 +20990,9 @@ watchpack@^1.7.4: watchpack-chokidar2 "^2.0.1" watchpack@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -20625,30 +21262,30 @@ which-builtin-type@^1.1.3: which-typed-array "^1.1.9" which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" which-module@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.2, which-typed-array@^1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.14.tgz#1f78a111aee1e131ca66164d8bdc3ab062c95a06" - integrity sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg== +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== dependencies: - available-typed-arrays "^1.0.6" - call-bind "^1.0.5" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-tostringtag "^1.0.1" + has-tostringtag "^1.0.2" which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" @@ -21026,3 +21663,8 @@ yup@1.3.3: tiny-case "^1.0.3" toposort "^2.0.2" type-fest "^2.19.0" + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==